diff --git a/plug-api/app_event.ts b/plug-api/app_event.ts index 47cace92..49b08f25 100644 --- a/plug-api/app_event.ts +++ b/plug-api/app_event.ts @@ -46,6 +46,11 @@ export type PublishEvent = { name: string; }; +export type LintEvent = { + name: string; + tree: ParseTree; +}; + export type CompleteEvent = { pageName: string; linePrefix: string; diff --git a/plugs/index/lint.ts b/plugs/index/lint.ts index 3bd8ec01..481506a3 100644 --- a/plugs/index/lint.ts +++ b/plugs/index/lint.ts @@ -1,14 +1,13 @@ -import { editor, markdown, YAML } from "$sb/syscalls.ts"; +import { YAML } from "$sb/syscalls.ts"; import { LintDiagnostic } from "$sb/types.ts"; import { findNodeOfType, renderToText, traverseTreeAsync, } from "$sb/lib/tree.ts"; +import { LintEvent } from "$sb/app_event.ts"; -export async function lintYAML(): Promise { - const text = await editor.getText(); - const tree = await markdown.parseMarkdown(text); +export async function lintYAML({ tree }: LintEvent): Promise { const diagnostics: LintDiagnostic[] = []; await traverseTreeAsync(tree, async (node) => { if (node.type === "FrontMatterCode") { diff --git a/plugs/query/query.plug.yaml b/plugs/query/query.plug.yaml index 7d7d31fa..c8ac1e73 100644 --- a/plugs/query/query.plug.yaml +++ b/plugs/query/query.plug.yaml @@ -4,6 +4,11 @@ functions: path: query.ts:widget codeWidget: query + lintQuery: + path: query.ts:lintQuery + events: + - editor:lint + templateWidget: path: template.ts:widget codeWidget: template diff --git a/plugs/query/query.ts b/plugs/query/query.ts index 3c259988..4b9f56de 100644 --- a/plugs/query/query.ts +++ b/plugs/query/query.ts @@ -1,10 +1,15 @@ -import type { WidgetContent } from "$sb/app_event.ts"; -import { events, language, system } from "$sb/syscalls.ts"; -import { parseTreeToAST } from "$sb/lib/tree.ts"; +import type { LintEvent, WidgetContent } from "$sb/app_event.ts"; +import { events, language, space, system } from "$sb/syscalls.ts"; +import { + findNodeOfType, + parseTreeToAST, + traverseTreeAsync, +} from "$sb/lib/tree.ts"; import { astToKvQuery } from "$sb/lib/parse-query.ts"; import { jsonToMDTable, renderQueryTemplate } from "../directive/util.ts"; import { loadPageObject, replaceTemplateVars } from "../template/template.ts"; -import { resolvePath } from "$sb/lib/resolve.ts"; +import { cleanPageRef, resolvePath } from "$sb/lib/resolve.ts"; +import { LintDiagnostic } from "$sb/types.ts"; export async function widget( bodyText: string, @@ -73,3 +78,90 @@ export async function widget( ); } } + +export async function lintQuery( + { name, tree }: LintEvent, +): Promise { + const pageObject = await loadPageObject(name); + const diagnostics: LintDiagnostic[] = []; + await traverseTreeAsync(tree, async (node) => { + if (node.type === "FencedCode") { + const codeInfo = findNodeOfType(node, "CodeInfo")!; + if (!codeInfo) { + return true; + } + const codeLang = codeInfo.children![0].text!; + if ( + codeLang !== "query" + ) { + return true; + } + const codeText = findNodeOfType(node, "CodeText"); + if (!codeText) { + return true; + } + const bodyText = codeText.children![0].text!; + try { + const queryAST = parseTreeToAST( + await language.parseLanguage( + "query", + await replaceTemplateVars(bodyText, pageObject), + ), + ); + const parsedQuery = astToKvQuery(queryAST[1]); + const allSources = await allQuerySources(); + if ( + parsedQuery.querySource && + !allSources.includes(parsedQuery.querySource) + ) { + diagnostics.push({ + from: codeText.from!, + to: codeText.to!, + message: `Unknown query source '${parsedQuery.querySource}'`, + severity: "error", + }); + } + if (parsedQuery.render) { + const templatePage = resolvePath( + name, + cleanPageRef(parsedQuery.render), + ); + try { + await space.getPageMeta(templatePage); + } catch (e: any) { + diagnostics.push({ + from: codeText.from!, + to: codeText.to!, + message: `Could not resolve template ${templatePage}`, + severity: "error", + }); + } + } + } catch (e: any) { + diagnostics.push({ + from: codeText.from!, + to: codeText.to!, + message: e.message, + severity: "error", + }); + } + } + return false; + }); + return diagnostics; +} + +async function allQuerySources(): Promise { + const allEvents = await events.listEvents(); + + const allSources = allEvents + .filter((eventName) => + eventName.startsWith("query:") && !eventName.includes("*") + ) + .map((source) => source.substring("query:".length)); + + const allObjectTypes: string[] = (await events.dispatchEvent("query_", {})) + .flat(); + + return [...allSources, ...allObjectTypes]; +} diff --git a/web/cm_plugins/lint.ts b/web/cm_plugins/lint.ts index de580d39..5e11b981 100644 --- a/web/cm_plugins/lint.ts +++ b/web/cm_plugins/lint.ts @@ -1,11 +1,19 @@ import { Diagnostic, linter } from "@codemirror/lint"; import type { Client } from "../client.ts"; +import { parse } from "../../common/markdown_parser/parse_tree.ts"; +import buildMarkdown from "../../common/markdown_parser/parser.ts"; +import { LintEvent } from "$sb/app_event.ts"; export function plugLinter(client: Client) { - return linter(async (): Promise => { + return linter(async (view): Promise => { + const tree = parse( + buildMarkdown(client.system.mdExtensions), + view.state.sliceDoc(), + ); const results = (await client.dispatchAppEvent("editor:lint", { name: client.currentPage!, - })).flat(); + tree: tree, + } as LintEvent)).flat(); return results; }); } diff --git a/website/CHANGELOG.md b/website/CHANGELOG.md index 5c03852f..66f79827 100644 --- a/website/CHANGELOG.md +++ b/website/CHANGELOG.md @@ -1,6 +1,14 @@ An attempt at documenting the changes/new features introduced in each release. +--- + +## Next +* Upgraded set of emoji (completed via the :thinking_face: syntax) to 15.1 (so more emoji) +* General support for highlighting errors (underlined) in the editor. Currently implemented for: + * All YAML fenced code blocks (and [[Frontmatter]]): will now highlight YAML parse errors + * [[Live Queries]]: will highlight non-existing query sources and non-existing template references in `render` clauses + --- ## 0.5.6 * Various optimization and bug fixes