Lint: support rich HTML tooltips

LintDiagnostic adds `messageHtml` (rendered into the hover
  tooltip) and `markClass` (added to the inline range mark)
This commit is contained in:
Zef Hemel
2026-05-09 12:29:19 +02:00
parent 3b22827aaf
commit 09059aa18a
4 changed files with 62 additions and 3 deletions
+34 -3
View File
@@ -1,14 +1,21 @@
import { type Diagnostic, linter } from "@codemirror/lint";
import { StateEffect } from "@codemirror/state";
import type { Client } from "../client.ts";
import { parse } from "../markdown_parser/parse_tree.ts";
import { extendedMarkdownLanguage } from "../markdown_parser/parser.ts";
import type { LintEvent } from "@silverbulletmd/silverbullet/type/client";
import type {
LintDiagnostic,
LintEvent,
} from "@silverbulletmd/silverbullet/type/client";
/** Signals the linter to re-run even when the document hasn't changed. */
export const refreshLintEffect = StateEffect.define<null>();
export function plugLinter(client: Client) {
return linter(async (view): Promise<Diagnostic[]> => {
const text = view.state.sliceDoc();
const tree = parse(extendedMarkdownLanguage, text);
const results = (
const results: LintDiagnostic[] = (
await client.dispatchAppEvent("editor:lint", {
name: client.currentName(),
pageMeta: client.currentPageMeta(),
@@ -16,6 +23,30 @@ export function plugLinter(client: Client) {
text,
} as LintEvent)
).flat();
return results;
return results.map(toDiagnostic);
}, {
needsRefresh: (update) =>
update.transactions.some((tr) =>
tr.effects.some((e) => e.is(refreshLintEffect))
),
});
}
function toDiagnostic(d: LintDiagnostic): Diagnostic {
const out: Diagnostic = {
from: d.from,
to: d.to,
severity: d.severity,
message: d.message,
};
if (d.markClass) out.markClass = d.markClass;
if (d.messageHtml) {
const html = d.messageHtml;
out.renderMessage = () => {
const wrap = document.createElement("div");
wrap.innerHTML = html;
return wrap;
};
}
return out;
}
+11
View File
@@ -71,6 +71,8 @@ import type {
UploadFile,
} from "@silverbulletmd/silverbullet/type/client";
import { openSearchPanel } from "@codemirror/search";
import { forceLinting } from "@codemirror/lint";
import { refreshLintEffect } from "../../codemirror/lint.ts";
import {
isValidPath,
parseToRef,
@@ -249,6 +251,15 @@ export function editorSyscalls(client: Client): SysCallMapping {
"editor.reloadPage": async () => {
await client.reloadEditor();
},
"editor.forceLint": () => {
// `forceLinting` alone is a no-op on unchanged content after the
// linter's internal "scheduled" flag has been cleared; the effect
// re-arms it via `needsRefresh`.
client.editorView.dispatch({
effects: refreshLintEffect.of(null),
});
forceLinting(client.editorView);
},
"editor.reloadUI": () => {
location.reload();
},
+5
View File
@@ -153,6 +153,11 @@ export function reloadPage(): Promise<void> {
return syscall("editor.reloadPage");
}
/** Useful when a plug toggles a piece of state that lint subscribers consult. */
export function forceLint(): Promise<void> {
return syscall("editor.forceLint");
}
/**
* Force reloads the browser UI
*/
+12
View File
@@ -54,6 +54,18 @@ export type LintDiagnostic = {
to: number;
severity: "error" | "warning" | "info" | "hint";
message: string;
/**
* Optional HTML rendered into the hover tooltip for this diagnostic.
* When present, takes precedence over `message` for tooltip display.
* `message` is still used as a plain-text fallback (e.g. in the lint
* panel) and as the diagnostic's accessible label.
*/
messageHtml?: string;
/**
* Optional CSS class added to the inline range mark for this diagnostic,
* in addition to CodeMirror's default `.cm-lintRange-<severity>` class.
*/
markClass?: string;
};
export type UploadFile = {