Fixes #539 linter support, now checking YAML formats

pull/570/head
Zef Hemel 2023-11-21 16:24:20 +01:00
parent aa5bff548d
commit 2aed9e5685
9 changed files with 230 additions and 36 deletions

View File

@ -12,7 +12,7 @@
"@codemirror/commands": "https://esm.sh/@codemirror/commands@6.2.4?external=@codemirror/state,@codemirror/view&target=es2022",
"@codemirror/view": "https://esm.sh/@codemirror/view@6.9.0?external=@codemirror/state,@lezer/common&target=es2022",
"@codemirror/autocomplete": "https://esm.sh/@codemirror/autocomplete@6.7.1?external=@codemirror/state,@codemirror/commands,@lezer/common,@codemirror/view&target=es2022",
"@codemirror/lint": "https://esm.sh/@codemirror/lint@6.2.1?external=@codemirror/state,@lezer/common&target=es2022",
"@codemirror/lint": "https://esm.sh/@codemirror/lint@6.4.1?external=@codemirror/state,@codemirror/view,@lezer/common&target=es2022",
"@codemirror/lang-css": "https://esm.sh/@codemirror/lang-css@6.2.0?external=@codemirror/language,@codemirror/autocomplete,@codemirror/state,@lezer/lr,@lezer/html&target=es2022",
"@codemirror/lang-html": "https://esm.sh/@codemirror/lang-html@6.4.3?external=@codemirror/language,@codemirror/autocomplete,@codemirror/lang-css,@codemirror/state,@lezer/lr,@lezer/html&target=es2022",
"@codemirror/search": "https://esm.sh/@codemirror/search@6.4.0?external=@codemirror/state,@codemirror/view&target=es2022",

View File

@ -7,6 +7,7 @@ export type AppEvent =
| "editor:complete"
| "minieditor:complete"
| "slash:complete"
| "editor:lint"
| "page:load"
| "editor:init"
| "editor:pageLoaded" // args: pageName, previousPage, isSynced

View File

@ -133,3 +133,10 @@ export type CodeWidgetContent = {
markdown?: string;
script?: string;
};
export type LintDiagnostic = {
from: number;
to: number;
severity: "error" | "warning" | "info" | "hint";
message: string;
};

View File

@ -174,3 +174,8 @@ functions:
renderMentions:
path: "./mentions_ps.ts:renderMentions"
lintYAML:
path: lint.ts:lintYAML
events:
- editor:lint

83
plugs/index/lint.ts Normal file
View File

@ -0,0 +1,83 @@
import { editor, markdown, YAML } from "$sb/syscalls.ts";
import { LintDiagnostic } from "$sb/types.ts";
import {
findNodeOfType,
renderToText,
traverseTreeAsync,
} from "$sb/lib/tree.ts";
export async function lintYAML(): Promise<LintDiagnostic[]> {
const text = await editor.getText();
const tree = await markdown.parseMarkdown(text);
const diagnostics: LintDiagnostic[] = [];
await traverseTreeAsync(tree, async (node) => {
if (node.type === "FrontMatterCode") {
const lintResult = await lintYaml(
renderToText(node),
node.from!,
);
if (lintResult) {
diagnostics.push(lintResult);
}
return true;
}
if (node.type === "FencedCode") {
const codeInfo = findNodeOfType(node, "CodeInfo")!;
if (!codeInfo) {
return true;
}
const codeLang = codeInfo.children![0].text!;
// All known YAML formats
if (
codeLang === "template" || codeLang === "yaml" ||
codeLang.startsWith("#")
) {
const codeText = findNodeOfType(node, "CodeText");
if (!codeText) {
return true;
}
const yamlCode = renderToText(codeText);
const lintResult = await lintYaml(
yamlCode,
codeText.from!,
);
if (lintResult) {
diagnostics.push(lintResult);
}
return true;
}
}
return false;
});
return diagnostics;
}
const errorRegex = /at line (\d+),? column (\d+)/;
async function lintYaml(
yamlText: string,
from: number,
): Promise<LintDiagnostic | undefined> {
try {
await YAML.parse(yamlText);
} catch (e) {
const errorMatch = errorRegex.exec(e.message);
if (errorMatch) {
console.log("YAML error", e.message);
const line = parseInt(errorMatch[1], 10) - 1;
const yamlLines = yamlText.split("\n");
let pos = from;
for (let i = 0; i < line; i++) {
pos += yamlLines[i].length + 1;
}
const endPos = pos + yamlLines[line]?.length || pos;
return {
from: pos,
to: endPos,
severity: "error",
message: e.message,
};
}
}
}

View File

@ -1,10 +1,15 @@
import type { IndexTreeEvent } from "$sb/app_event.ts";
import { space } from "$sb/syscalls.ts";
import { editor, markdown, space, YAML } from "$sb/syscalls.ts";
import type { PageMeta } from "$sb/types.ts";
import type { LintDiagnostic, PageMeta } from "$sb/types.ts";
import { extractFrontmatter } from "$sb/lib/frontmatter.ts";
import { extractAttributes } from "$sb/lib/attribute.ts";
import { indexObjects } from "./api.ts";
import {
findNodeOfType,
renderToText,
traverseTreeAsync,
} from "$sb/lib/tree.ts";
export async function indexPage({ name, tree }: IndexTreeEvent) {
if (name.startsWith("_")) {
@ -22,7 +27,84 @@ export async function indexPage({ name, tree }: IndexTreeEvent) {
pageMeta.tags = [...new Set(["page", ...pageMeta.tags || []])];
// console.log("Page object", pageObj);
// console.log("Extracted page meta data", pageMeta);
await indexObjects<PageMeta>(name, [pageMeta]);
}
export async function lintFrontmatter(): Promise<LintDiagnostic[]> {
const text = await editor.getText();
const tree = await markdown.parseMarkdown(text);
const diagnostics: LintDiagnostic[] = [];
await traverseTreeAsync(tree, async (node) => {
if (node.type === "FrontMatterCode") {
const lintResult = await lintYaml(
renderToText(node),
node.from!,
node.to!,
);
if (lintResult) {
diagnostics.push(lintResult);
}
return true;
}
if (node.type === "FencedCode") {
const codeInfo = findNodeOfType(node, "CodeInfo")!;
if (!codeInfo) {
return true;
}
const codeLang = codeInfo.children![0].text!;
// All known YAML formats
if (
codeLang === "template" || codeLang === "yaml" ||
codeLang.startsWith("#")
) {
const codeText = findNodeOfType(node, "CodeText");
if (!codeText) {
return true;
}
const yamlCode = renderToText(codeText);
const lintResult = await lintYaml(
yamlCode,
codeText.from!,
codeText.to!,
);
if (lintResult) {
diagnostics.push(lintResult);
}
return true;
}
}
return false;
});
return diagnostics;
}
const errorRegex = /at line (\d+),? column (\d+)/;
async function lintYaml(
yamlText: string,
from: number,
to: number,
): Promise<LintDiagnostic | undefined> {
try {
await YAML.parse(yamlText);
} catch (e) {
const errorMatch = errorRegex.exec(e.message);
if (errorMatch) {
console.log("YAML error", e.message);
// const line = parseInt(errorMatch[1], 10) - 1;
// const yamlLines = yamlText.split("\n");
// let pos = posOffset;
// for (let i = 0; i < line; i++) {
// pos += yamlLines[i].length + 1;
// }
// const endPos = pos + yamlLines[line].length;
return {
from,
to,
severity: "error",
message: e.message,
};
}
}
}

View File

@ -27,7 +27,7 @@ class AdmonitionIconWidget extends WidgetType {
toDOM(): HTMLElement {
const outerDiv = document.createElement("div");
outerDiv.classList.add("sb-admonition-icon");
outerDiv.addEventListener("click", (e) => {
outerDiv.addEventListener("click", () => {
this.editorView.dispatch({
selection: {
anchor: this.pos,

11
web/cm_plugins/lint.ts Normal file
View File

@ -0,0 +1,11 @@
import { Diagnostic, linter } from "@codemirror/lint";
import type { Client } from "../client.ts";
export function plugLinter(client: Client) {
return linter(async (): Promise<Diagnostic[]> => {
const results = (await client.dispatchAppEvent("editor:lint", {
name: client.currentPage!,
})).flat();
return results;
});
}

View File

@ -42,22 +42,23 @@ import {
import { TextChange } from "$sb/lib/change.ts";
import { postScriptPlugin } from "./cm_plugins/post_script.ts";
import { languageFor } from "../common/languages.ts";
import { plugLinter } from "./cm_plugins/lint.ts";
export function createEditorState(
editor: Client,
client: Client,
pageName: string,
text: string,
readOnly: boolean,
): EditorState {
const commandKeyBindings: KeyBinding[] = [];
for (const def of editor.system.commandHook.editorCommands.values()) {
for (const def of client.system.commandHook.editorCommands.values()) {
if (def.command.key) {
commandKeyBindings.push({
key: def.command.key,
mac: def.command.mac,
run: (): boolean => {
if (def.command.contexts) {
const context = editor.getContext();
const context = client.getContext();
if (!context || !def.command.contexts.includes(context)) {
return false;
}
@ -66,14 +67,14 @@ export function createEditorState(
.then(def.run)
.catch((e: any) => {
console.error(e);
editor.flashNotification(
client.flashNotification(
`Error running command: ${e.message}`,
"error",
);
})
.then(() => {
// Always be focusing the editor after running a command
editor.focus();
client.focus();
});
return true;
},
@ -82,24 +83,25 @@ export function createEditorState(
}
let touchCount = 0;
const markdownLanguage = buildMarkdown(editor.system.mdExtensions);
const markdownLanguage = buildMarkdown(client.system.mdExtensions);
return EditorState.create({
doc: text,
extensions: [
// Not using CM theming right now, but some extensions depend on the "dark" thing
EditorView.theme({}, {
dark: editor.ui.viewState.uiOptions.darkMode,
dark: client.ui.viewState.uiOptions.darkMode,
}),
// Enable vim mode, or not
[
...editor.ui.viewState.uiOptions.vimMode ? [vim({ status: true })] : [],
...client.ui.viewState.uiOptions.vimMode ? [vim({ status: true })] : [],
],
[
...readOnly || editor.ui.viewState.uiOptions.forcedROMode
...readOnly || client.ui.viewState.uiOptions.forcedROMode
? [readonlyMode()]
: [],
],
// The uber markdown mode
markdown({
base: markdownLanguage,
@ -119,16 +121,16 @@ export function createEditorState(
markdownLanguage.data.of({
closeBrackets: { brackets: ["(", "{", "[", "`"] },
}),
syntaxHighlighting(customMarkdownStyle(editor.system.mdExtensions)),
syntaxHighlighting(customMarkdownStyle(client.system.mdExtensions)),
autocompletion({
override: [
editor.editorComplete.bind(editor),
editor.system.slashCommandHook.slashCommandCompleter.bind(
editor.system.slashCommandHook,
client.editorComplete.bind(client),
client.system.slashCommandHook.slashCommandCompleter.bind(
client.system.slashCommandHook,
),
],
}),
inlineImagesPlugin(editor),
inlineImagesPlugin(client),
highlightSpecialChars(),
history(),
drawSelection(),
@ -137,9 +139,12 @@ export function createEditorState(
placeholderText: "…",
}),
indentOnInput(),
...cleanModePlugins(editor),
...cleanModePlugins(client),
EditorView.lineWrapping,
postScriptPlugin(editor),
plugLinter(client),
// lintGutter(),
// gutters(),
postScriptPlugin(client),
lineWrapper([
{ selector: "ATXHeading1", class: "sb-line-h1" },
{ selector: "ATXHeading2", class: "sb-line-h2" },
@ -173,8 +178,8 @@ export function createEditorState(
key: "Ctrl-k",
mac: "Cmd-k",
run: (): boolean => {
editor.ui.viewDispatch({ type: "start-navigate" });
editor.space.updatePageList();
client.ui.viewDispatch({ type: "start-navigate" });
client.space.updatePageList();
return true;
},
@ -183,9 +188,9 @@ export function createEditorState(
key: "Ctrl-/",
mac: "Cmd-/",
run: (): boolean => {
editor.ui.viewDispatch({
client.ui.viewDispatch({
type: "show-palette",
context: editor.getContext(),
context: client.getContext(),
});
return true;
},
@ -194,9 +199,9 @@ export function createEditorState(
key: "Ctrl-.",
mac: "Cmd-.",
run: (): boolean => {
editor.ui.viewDispatch({
client.ui.viewDispatch({
type: "show-palette",
context: editor.getContext(),
context: client.getContext(),
});
return true;
},
@ -229,7 +234,7 @@ export function createEditorState(
y: touch.clientY,
})!,
};
await editor.dispatchAppEvent("page:click", clickEvent);
await client.dispatchAppEvent("page:click", clickEvent);
});
}
touchCount = 0;
@ -257,7 +262,7 @@ export function createEditorState(
if (parentA) {
event.stopPropagation();
event.preventDefault();
await editor.dispatchAppEvent(
await client.dispatchAppEvent(
"page:click",
potentialClickEvent,
);
@ -270,7 +275,7 @@ export function createEditorState(
// this may not be the case with locations that expand signifcantly based on live preview (such as links), we don't want any accidental clicks
// Fixes #357
if (distanceX <= view.defaultCharacterWidth) {
await editor.dispatchAppEvent("page:click", potentialClickEvent);
await client.dispatchAppEvent("page:click", potentialClickEvent);
}
});
},
@ -287,16 +292,16 @@ export function createEditorState(
newRange: { from: fromB, to: toB },
})
);
editor.dispatchAppEvent("editor:pageModified", { changes });
editor.ui.viewDispatch({ type: "page-changed" });
editor.debouncedUpdateEvent();
editor.save().catch((e) => console.error("Error saving", e));
client.dispatchAppEvent("editor:pageModified", { changes });
client.ui.viewDispatch({ type: "page-changed" });
client.debouncedUpdateEvent();
client.save().catch((e) => console.error("Error saving", e));
}
}
},
),
pasteLinkExtension,
attachmentExtension(editor),
attachmentExtension(client),
closeBrackets(),
],
});