diff --git a/e2e/src/command.test.ts b/e2e/src/command.test.ts index 482c37b2..103afe18 100644 --- a/e2e/src/command.test.ts +++ b/e2e/src/command.test.ts @@ -68,11 +68,9 @@ describe("Initialization testing", function() { async function notificationExists(workbench: Workbench, text: string): Promise { const notifications = await (await (new StatusBar()).openNotificationsCenter()).getNotifications(NotificationType.Info); - console.log(`Notifications:`, notifications.length); for (const notification of notifications) { const message = await notification.getMessage(); - console.log(message) if (message.indexOf(text) >= 0) { return notification; } diff --git a/src/commands/Dashboard.ts b/src/commands/Dashboard.ts index b99c44ee..d34e682f 100644 --- a/src/commands/Dashboard.ts +++ b/src/commands/Dashboard.ts @@ -131,7 +131,7 @@ export class Dashboard { }); SettingsHelper.onConfigChange(() => { - SettingsListener.getSettings(); + SettingsListener.getSettings(true); }); Dashboard.webview.webview.onDidReceiveMessage(async (msg) => { diff --git a/src/commands/Folders.ts b/src/commands/Folders.ts index c12977fd..ea39ae10 100644 --- a/src/commands/Folders.ts +++ b/src/commands/Folders.ts @@ -137,7 +137,7 @@ export class Folders { Telemetry.send(TelemetryEvent.registerFolder); - SettingsListener.getSettings(); + SettingsListener.getSettings(true); } } diff --git a/src/commands/Project.ts b/src/commands/Project.ts index 8e708f4c..d7cb3488 100644 --- a/src/commands/Project.ts +++ b/src/commands/Project.ts @@ -55,7 +55,7 @@ categories: [] SettingsListener.setFramework(framework.name); } - SettingsListener.getSettings(); + SettingsListener.getSettings(true); } catch (err: any) { Logger.error(`Project::init: ${err?.message || err}`); Notifications.error(`Sorry, something went wrong - ${err?.message || err}`); diff --git a/src/extension.ts b/src/extension.ts index c463e958..f949b35c 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -15,7 +15,7 @@ import { TagType } from './panelWebView/TagType'; import { ExplorerView } from './explorerView/ExplorerView'; import { Extension } from './helpers/Extension'; import { DashboardData } from './models/DashboardData'; -import { debounceCallback, Logger, Settings as SettingsHelper } from './helpers'; +import { DashboardSettings, debounceCallback, Logger, Settings as SettingsHelper } from './helpers'; import { Content } from './commands/Content'; import ContentProvider from './providers/ContentProvider'; import { Wysiwyg } from './commands/Wysiwyg'; @@ -25,6 +25,7 @@ import { Backers } from './commands/Backers'; import { DataListener, SettingsListener } from './listeners/panel'; import { NavigationType } from './dashboardWebView/models'; import { ModeSwitch } from './services/ModeSwitch'; +import { PagesParser } from './services/PagesParser'; let frontMatterStatusBar: vscode.StatusBarItem; let statusDebouncer: { (fnc: any, time: number): void; }; @@ -266,6 +267,10 @@ export async function activate(context: vscode.ExtensionContext) { // Git GitListener.init(); + // Once everything is registered, the page parsing can start in the background + DashboardSettings.get(); + PagesParser.start(); + // Subscribe all commands subscriptions.push( insertTags, diff --git a/src/helpers/DashboardSettings.ts b/src/helpers/DashboardSettings.ts index c40cc197..cd254173 100644 --- a/src/helpers/DashboardSettings.ts +++ b/src/helpers/DashboardSettings.ts @@ -16,15 +16,24 @@ import { parseWinPath } from './parseWinPath'; export class DashboardSettings { + private static cachedSettings: ISettings | undefined = undefined; - public static async get() { + public static async get(clear: boolean = false) { + if (!this.cachedSettings || clear) { + this.cachedSettings = await this.getSettings(); + } + + return this.cachedSettings; + } + + public static async getSettings() { const ext = Extension.getInstance(); const wsFolder = Folders.getWorkspaceFolder(); const isInitialized = Project.isInitialized(); const gitActions = Settings.get(SETTING_GIT_ENABLED); const pagination = Settings.get(SETTING_DASHBOARD_CONTENT_PAGINATION) - return { + const settings = { git: { isGitRepo: gitActions ? await GitListener.isGitRepository() : false, actions: gitActions || false @@ -71,7 +80,9 @@ export class DashboardSettings { dataTypes: Settings.get(SETTING_DATA_TYPES), snippets: Settings.get(SETTING_CONTENT_SNIPPETS), isBacker: await ext.getState(CONTEXT.backer, 'global') - } as ISettings + } as ISettings; + + return settings; } /** diff --git a/src/listeners/dashboard/PagesListener.ts b/src/listeners/dashboard/PagesListener.ts index af42d614..a7b92fe5 100644 --- a/src/listeners/dashboard/PagesListener.ts +++ b/src/listeners/dashboard/PagesListener.ts @@ -1,21 +1,17 @@ -import { DEFAULT_CONTENT_TYPE_NAME } from './../../constants/ContentType'; -import { isValidFile } from '../../helpers/isValidFile'; -import { existsSync, unlinkSync } from "fs"; -import { basename, dirname, join } from "path"; +import { unlinkSync } from "fs"; +import { basename } from "path"; import { commands, FileSystemWatcher, RelativePattern, TextDocument, Uri, workspace } from "vscode"; import { Dashboard } from "../../commands/Dashboard"; import { Folders } from "../../commands/Folders"; -import { COMMAND_NAME, DefaultFields, ExtensionState, SETTING_SEO_DESCRIPTION_FIELD } from "../../constants"; +import { COMMAND_NAME, ExtensionState } from "../../constants"; import { DashboardCommand } from "../../dashboardWebView/DashboardCommand"; import { DashboardMessage } from "../../dashboardWebView/DashboardMessage"; import { Page } from "../../dashboardWebView/models"; -import { ArticleHelper, Extension, Logger, Settings } from "../../helpers"; -import { ContentType } from "../../helpers/ContentType"; -import { DateHelper } from "../../helpers/DateHelper"; -import { Notifications } from "../../helpers/Notifications"; +import { ArticleHelper, Extension, Logger } from "../../helpers"; import { BaseListener } from "./BaseListener"; import { DataListener } from '../panel'; import Fuse from 'fuse.js'; +import { PagesParser } from '../../services/PagesParser'; export class PagesListener extends BaseListener { @@ -132,7 +128,7 @@ export class PagesListener extends BaseListener { if (pageIdx !== -1) { const stats = await workspace.fs.stat(file); const crntPage = this.lastPages[pageIdx]; - const updatedPage = this.processPageContent(file.fsPath, stats.mtime, basename(file.fsPath), crntPage.fmFolder); + const updatedPage = PagesParser.processPageContent(file.fsPath, stats.mtime, basename(file.fsPath), crntPage.fmFolder); if (updatedPage) { this.lastPages[pageIdx] = updatedPage; this.sendPageData(this.lastPages); @@ -156,43 +152,19 @@ export class PagesListener extends BaseListener { if (cachedPages) { this.sendPageData(cachedPages); } + } else { + PagesParser.reset(); } - // Update the dashboard with the fresh data - const folderInfo = await Folders.getInfo(); - const pages: Page[] = []; + PagesParser.getPages(async (pages: Page[]) => { + this.lastPages = pages; + this.sendPageData(pages); - if (folderInfo) { - for (const folder of folderInfo) { - for (const file of folder.lastModified) { - if (isValidFile(file.fileName)) { - try { - const page = this.processPageContent(file.filePath, file.mtime, file.fileName, folder.title); + this.sendMsg(DashboardCommand.searchReady, true); - if (page && !pages.find(p => p.fmFilePath === page.fmFilePath)) { - pages.push(page); - } - - } catch (error: any) { - if ((error as Error)?.message.toLowerCase() === "webview is disposed") { - continue; - } - - Logger.error(`PagesListener::getPagesData: ${file.filePath} - ${error.message}`); - Notifications.error(`File error: ${file.filePath} - ${error?.message || error}`); - } - } - } - } - } - - this.lastPages = pages; - this.sendPageData(pages); - - this.sendMsg(DashboardCommand.searchReady, true); - - await ext.setState(ExtensionState.Dashboard.Pages.Cache, pages, "workspace"); - await this.createSearchIndex(pages); + await ext.setState(ExtensionState.Dashboard.Pages.Cache, pages, "workspace"); + await this.createSearchIndex(pages); + }); } /** @@ -245,136 +217,4 @@ export class PagesListener extends BaseListener { public static refresh() { this.getPagesData(true); } - - /** - * Process the page content - * @param filePath - * @param fileMtime - * @param fileName - * @param folderTitle - * @returns - */ - private static processPageContent(filePath: string, fileMtime: number, fileName: string, folderTitle: string): Page | undefined { - const article = ArticleHelper.getFrontMatterByPath(filePath); - - if (article?.data.title) { - const wsFolder = Folders.getWorkspaceFolder(); - const descriptionField = Settings.get(SETTING_SEO_DESCRIPTION_FIELD) as string || DefaultFields.Description; - - const dateField = ArticleHelper.getPublishDateField(article) || DefaultFields.PublishingDate; - const dateFieldValue = article?.data[dateField] ? DateHelper.tryParse(article?.data[dateField]) : undefined; - - const modifiedField = ArticleHelper.getModifiedDateField(article) || null; - const modifiedFieldValue = modifiedField && article?.data[modifiedField] ? DateHelper.tryParse(article?.data[modifiedField])?.getTime() : undefined; - - const staticFolder = Folders.getStaticFolderRelativePath(); - - const page: Page = { - ...article.data, - // FrontMatter properties - fmFolder: folderTitle, - fmFilePath: filePath, - fmFileName: fileName, - fmDraft: ContentType.getDraftStatus(article?.data), - fmModified: modifiedFieldValue ? modifiedFieldValue : fileMtime, - fmPublished: dateFieldValue ? dateFieldValue.getTime() : null, - fmYear: dateFieldValue ? dateFieldValue.getFullYear() : null, - fmPreviewImage: "", - fmTags: [], - fmCategories: [], - fmContentType: DEFAULT_CONTENT_TYPE_NAME, - fmBody: article?.content || "", - // Make sure these are always set - title: article?.data.title, - slug: article?.data.slug, - date: article?.data[dateField] || "", - draft: article?.data.draft, - description: article?.data[descriptionField] || "", - }; - - const contentType = ArticleHelper.getContentType(article.data); - if (contentType) { - page.fmContentType = contentType.name; - } - - let previewFieldParents = ContentType.findPreviewField(contentType.fields); - if (previewFieldParents.length === 0) { - const previewField = contentType.fields.find(field => field.type === "image" && field.name === "preview"); - if (previewField) { - previewFieldParents = ["preview"]; - } - } - - let tagParents = ContentType.findFieldByType(contentType.fields, "tags"); - const tagsValue = ContentType.getFieldValue(article.data, tagParents.length !== 0 ? tagParents : ["tags"]); - page.fmTags = typeof tagsValue === "string" ? tagsValue.split(",") : tagsValue; - - let categoryParents = ContentType.findFieldByType(contentType.fields, "categories"); - const categoriesValue = ContentType.getFieldValue(article.data, categoryParents.length !== 0 ? categoryParents : ["categories"]); - page.fmCategories = typeof categoriesValue === "string" ? categoriesValue.split(",") : categoriesValue; - - // Check if parent fields were retrieved, if not there was no image present - if (previewFieldParents.length > 0) { - let fieldValue = null; - let crntPageData = article?.data; - - for (let i = 0; i < previewFieldParents.length; i++) { - const previewField = previewFieldParents[i]; - - if (i === previewFieldParents.length - 1) { - fieldValue = crntPageData[previewField]; - } else { - if (!crntPageData[previewField]) { - continue; - } - - crntPageData = crntPageData[previewField]; - - // Check for preview image in block data - if (crntPageData instanceof Array && crntPageData.length > 0) { - // Get the first field block that contains the next field data - const fieldData = crntPageData.find(item => item[previewFieldParents[i + 1]]); - if (fieldData) { - crntPageData = fieldData; - } else { - continue; - } - } - } - } - - if (fieldValue && wsFolder) { - if (fieldValue && Array.isArray(fieldValue)) { - if (fieldValue.length > 0) { - fieldValue = fieldValue[0]; - } else { - fieldValue = undefined; - } - } - - // Revalidate as the array could have been empty - if (fieldValue) { - const staticPath = join(wsFolder.fsPath, staticFolder || "", fieldValue); - const contentFolderPath = join(dirname(filePath), fieldValue); - - let previewUri = null; - if (existsSync(staticPath)) { - previewUri = Uri.file(staticPath); - } else if (existsSync(contentFolderPath)) { - previewUri = Uri.file(contentFolderPath); - } - - if (previewUri) { - const preview = Dashboard.getWebview()?.asWebviewUri(previewUri); - page["fmPreviewImage"] = preview?.toString() || ""; - } - } - } - } - - return page; - } - - return; - } } \ No newline at end of file diff --git a/src/listeners/dashboard/SettingsListener.ts b/src/listeners/dashboard/SettingsListener.ts index 728c6356..e9c7ee9b 100644 --- a/src/listeners/dashboard/SettingsListener.ts +++ b/src/listeners/dashboard/SettingsListener.ts @@ -42,15 +42,15 @@ export class SettingsListener extends BaseListener { private static async update(data: { name: string, value: any }) { if (data.name) { await Settings.update(data.name, data.value); - this.getSettings(); + this.getSettings(true); } } /** * Retrieve the settings for the dashboard */ - public static async getSettings() { - const settings = await DashboardSettings.get(); + public static async getSettings(clear: boolean = false) { + const settings = await DashboardSettings.get(clear); this.sendMsg(DashboardCommand.settings, settings); } @@ -74,7 +74,7 @@ export class SettingsListener extends BaseListener { } } - SettingsListener.getSettings(); + SettingsListener.getSettings(true); } private static addFolder(folder: string) { diff --git a/src/listeners/dashboard/SnippetListener.ts b/src/listeners/dashboard/SnippetListener.ts index d1887c3b..f472f4cc 100644 --- a/src/listeners/dashboard/SnippetListener.ts +++ b/src/listeners/dashboard/SnippetListener.ts @@ -57,7 +57,7 @@ export class SnippetListener extends BaseListener { snippets[title] = snippetContent; await Settings.update(SETTING_CONTENT_SNIPPETS, snippets, true); - SettingsListener.getSettings(); + SettingsListener.getSettings(true); } private static async updateSnippet(data: any) { @@ -69,7 +69,7 @@ export class SnippetListener extends BaseListener { } await Settings.update(SETTING_CONTENT_SNIPPETS, snippets, true); - SettingsListener.getSettings(); + SettingsListener.getSettings(true); } private static async insertSnippet(data: any) { diff --git a/src/services/PagesParser.ts b/src/services/PagesParser.ts new file mode 100644 index 00000000..1bf31b8b --- /dev/null +++ b/src/services/PagesParser.ts @@ -0,0 +1,219 @@ +import { parseWinPath } from './../helpers/parseWinPath'; +import { existsSync } from "fs"; +import { dirname, join } from "path"; +import { StatusBarAlignment, Uri, window } from "vscode"; +import { Dashboard } from "../commands/Dashboard"; +import { Folders } from "../commands/Folders"; +import { DefaultFields, DEFAULT_CONTENT_TYPE_NAME, SETTING_SEO_DESCRIPTION_FIELD } from "../constants"; +import { Page } from "../dashboardWebView/models"; +import { ArticleHelper, ContentType, DateHelper, isValidFile, Logger, Notifications, Settings } from "../helpers"; + + +export class PagesParser { + public static allPages: Page[] = []; + private static parser: Promise | undefined; + private static initialized: boolean = false; + + public static start() { + if (!this.parser) { + this.parser = this.parsePages(); + } + } + + public static getPages(cb: (pages: Page[]) => void) { + if (this.parser) { + this.parser.then(() => cb(PagesParser.allPages)); + } else if (!PagesParser.initialized) { + this.parser = this.parsePages(); + this.parser.then(() => cb(PagesParser.allPages)); + } else if (PagesParser.allPages === undefined || PagesParser.allPages.length === 0) { + this.parser = this.parsePages(); + this.parser.then(() => cb(PagesParser.allPages)); + } else { + cb(PagesParser.allPages); + } + } + + public static async reset() { + this.parser = undefined; + PagesParser.allPages = []; + } + + public static async parsePages() { + // Update the dashboard with the fresh data + const folderInfo = await Folders.getInfo(); + const pages: Page[] = []; + const statusBar = window.createStatusBarItem(StatusBarAlignment.Left); + + if (folderInfo) { + statusBar.text = '$(sync~spin) Processing pages...'; + statusBar.show(); + + for (const folder of folderInfo) { + for (const file of folder.lastModified) { + if (isValidFile(file.fileName)) { + try { + const page = this.processPageContent(file.filePath, file.mtime, file.fileName, folder.title); + + if (page && !pages.find(p => p.fmFilePath === page.fmFilePath)) { + pages.push(page); + } + + } catch (error: any) { + if ((error as Error)?.message.toLowerCase() === "webview is disposed") { + continue; + } + + Logger.error(`PagesParser::parsePages: ${file.filePath} - ${error.message}`); + Notifications.error(`File error: ${file.filePath} - ${error?.message || error}`); + } + } + } + } + } + + this.parser = undefined; + this.initialized = true; + PagesParser.allPages = [...pages]; + statusBar.hide(); + } + + /** + * Process the page content + * @param filePath + * @param fileMtime + * @param fileName + * @param folderTitle + * @returns + */ + public static processPageContent(filePath: string, fileMtime: number, fileName: string, folderTitle: string): Page | undefined { + const article = ArticleHelper.getFrontMatterByPath(filePath); + + if (article?.data.title) { + const wsFolder = Folders.getWorkspaceFolder(); + const descriptionField = Settings.get(SETTING_SEO_DESCRIPTION_FIELD) as string || DefaultFields.Description; + + const dateField = ArticleHelper.getPublishDateField(article) || DefaultFields.PublishingDate; + const dateFieldValue = article?.data[dateField] ? DateHelper.tryParse(article?.data[dateField]) : undefined; + + const modifiedField = ArticleHelper.getModifiedDateField(article) || null; + const modifiedFieldValue = modifiedField && article?.data[modifiedField] ? DateHelper.tryParse(article?.data[modifiedField])?.getTime() : undefined; + + const staticFolder = Folders.getStaticFolderRelativePath(); + + const page: Page = { + ...article.data, + // FrontMatter properties + fmFolder: folderTitle, + fmFilePath: filePath, + fmFileName: fileName, + fmDraft: ContentType.getDraftStatus(article?.data), + fmModified: modifiedFieldValue ? modifiedFieldValue : fileMtime, + fmPublished: dateFieldValue ? dateFieldValue.getTime() : null, + fmYear: dateFieldValue ? dateFieldValue.getFullYear() : null, + fmPreviewImage: "", + fmTags: [], + fmCategories: [], + fmContentType: DEFAULT_CONTENT_TYPE_NAME, + fmBody: article?.content || "", + // Make sure these are always set + title: article?.data.title, + slug: article?.data.slug, + date: article?.data[dateField] || "", + draft: article?.data.draft, + description: article?.data[descriptionField] || "", + }; + + const contentType = ArticleHelper.getContentType(article.data); + if (contentType) { + page.fmContentType = contentType.name; + } + + let previewFieldParents = ContentType.findPreviewField(contentType.fields); + if (previewFieldParents.length === 0) { + const previewField = contentType.fields.find(field => field.type === "image" && field.name === "preview"); + if (previewField) { + previewFieldParents = ["preview"]; + } + } + + let tagParents = ContentType.findFieldByType(contentType.fields, "tags"); + const tagsValue = ContentType.getFieldValue(article.data, tagParents.length !== 0 ? tagParents : ["tags"]); + page.fmTags = typeof tagsValue === "string" ? tagsValue.split(",") : tagsValue; + + let categoryParents = ContentType.findFieldByType(contentType.fields, "categories"); + const categoriesValue = ContentType.getFieldValue(article.data, categoryParents.length !== 0 ? categoryParents : ["categories"]); + page.fmCategories = typeof categoriesValue === "string" ? categoriesValue.split(",") : categoriesValue; + + // Check if parent fields were retrieved, if not there was no image present + if (previewFieldParents.length > 0) { + let fieldValue = null; + let crntPageData = article?.data; + + for (let i = 0; i < previewFieldParents.length; i++) { + const previewField = previewFieldParents[i]; + + if (i === previewFieldParents.length - 1) { + fieldValue = crntPageData[previewField]; + } else { + if (!crntPageData[previewField]) { + continue; + } + + crntPageData = crntPageData[previewField]; + + // Check for preview image in block data + if (crntPageData instanceof Array && crntPageData.length > 0) { + // Get the first field block that contains the next field data + const fieldData = crntPageData.find(item => item[previewFieldParents[i + 1]]); + if (fieldData) { + crntPageData = fieldData; + } else { + continue; + } + } + } + } + + if (fieldValue && wsFolder) { + if (fieldValue && Array.isArray(fieldValue)) { + if (fieldValue.length > 0) { + fieldValue = fieldValue[0]; + } else { + fieldValue = undefined; + } + } + + // Revalidate as the array could have been empty + if (fieldValue) { + const staticPath = join(wsFolder.fsPath, staticFolder || "", fieldValue); + const contentFolderPath = join(dirname(filePath), fieldValue); + + let previewUri = null; + if (existsSync(staticPath)) { + previewUri = Uri.file(staticPath); + } else if (existsSync(contentFolderPath)) { + previewUri = Uri.file(contentFolderPath); + } + + if (previewUri) { + const previewPath = Dashboard.getWebview()?.asWebviewUri(previewUri); + let preview = previewPath?.toString(); + + if (!preview) { + const fileUrl = parseWinPath(previewUri.fsPath); + preview = `https://file%2B.vscode-resource.vscode-cdn.net/${fileUrl.startsWith(`/`) ? fileUrl.substr(1) : fileUrl}`; + } + + page["fmPreviewImage"] = preview?.toString() || ""; + } + } + } + } + + return page; + } + + return; + } +} \ No newline at end of file