Fixes #539 linter support, now checking YAML formats
parent
aa5bff548d
commit
2aed9e5685
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -174,3 +174,8 @@ functions:
|
|||
|
||||
renderMentions:
|
||||
path: "./mentions_ps.ts:renderMentions"
|
||||
|
||||
lintYAML:
|
||||
path: lint.ts:lintYAML
|
||||
events:
|
||||
- editor:lint
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
}
|
|
@ -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(),
|
||||
],
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue