mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-06 08:07:49 +00:00
feat(editor): mark syntax errors in files no language server checks (#1087)
Without a running language server (none configured for the language, or not installed, stopped or crashed), the editor showed no syntax errors at all, though the built-in parser already finds them. The editor now reports the parser's errors for such files, checked again after edits and when a language server starts or stops; while a server runs for the language it reports syntax errors itself. SQL is left to the database support. The check is the built-in Syntax errors inspection. Fixes #824
This commit is contained in:
@@ -32,6 +32,7 @@ import { registerActionsOnSave, registerFormatOnSave, registerLspActions } from
|
||||
import { lspTheme } from "./theme";
|
||||
import { inspectionWidgetExtension } from "./inspectionWidget";
|
||||
import { unresolvedReferences } from "./fileReferences";
|
||||
import { syntaxErrorsExtension } from "./syntaxErrors";
|
||||
import { spellingExtension } from "./spelling";
|
||||
import { DocumentationWindow, LspPopups, UsagesStatus } from "./Popups";
|
||||
import { DOCUMENTATION_WINDOW } from "./docs";
|
||||
@@ -58,6 +59,7 @@ export function lspExtensions(ctx: EditorDocContext): Extension[] {
|
||||
usagesLensExtension(),
|
||||
autoImportExtension(),
|
||||
unresolvedReferences(),
|
||||
syntaxErrorsExtension(),
|
||||
spellingExtension(),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import { EditorState } from "@codemirror/state";
|
||||
import { EditorView } from "@codemirror/view";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const published: Array<[string, string, unknown[]]> = [];
|
||||
let servers: Array<{ status: string; languages: string[] }> = [];
|
||||
const serverListeners: Array<() => void> = [];
|
||||
|
||||
vi.mock("@/platform/ipc", () => ({
|
||||
isTauri: () => true,
|
||||
call: (p: Promise<unknown>) => p,
|
||||
commands: { lspServers: () => Promise.resolve(servers) },
|
||||
onIdeEvent: (_type: string, l: () => void) => {
|
||||
serverListeners.push(l);
|
||||
return () => {};
|
||||
},
|
||||
}));
|
||||
vi.mock("@/editor/api", () => ({ whenSynced: () => Promise.resolve() }));
|
||||
vi.mock("@/editor/structure", () => ({
|
||||
structure: {
|
||||
syntaxErrors: () =>
|
||||
Promise.resolve({
|
||||
errors: [
|
||||
{
|
||||
kind: "unexpected",
|
||||
range: { start: { line: 1, character: 4 }, end: { line: 1, character: 10 } },
|
||||
expected: null,
|
||||
message: "unexpected `iqTask`",
|
||||
},
|
||||
{
|
||||
kind: "missing",
|
||||
range: { start: { line: 2, character: 3 }, end: { line: 2, character: 3 } },
|
||||
expected: ";",
|
||||
message: "missing `;`",
|
||||
},
|
||||
],
|
||||
truncated: false,
|
||||
}),
|
||||
},
|
||||
}));
|
||||
vi.mock("./diagnostics", () => ({
|
||||
setFileDiagnostics: (path: string, source: string, items: unknown[]) => published.push([path, source, items]),
|
||||
}));
|
||||
|
||||
const { lspDoc } = await import("./lsp");
|
||||
const { syntaxErrorsExtension } = await import("./syntaxErrors");
|
||||
const { SYNTAX_ERRORS_PRODUCER } = await import("@/platform/inspections");
|
||||
|
||||
const ctx = { doc: 1, path: "/p/a.php", language: "php", uiMode: null, readOnly: false };
|
||||
const views: EditorView[] = [];
|
||||
function open() {
|
||||
const view = new EditorView({
|
||||
state: EditorState.create({
|
||||
doc: "<?php\n$a = A::geUn iqTask\nb()\n",
|
||||
extensions: [lspDoc.of(ctx), syntaxErrorsExtension()],
|
||||
}),
|
||||
});
|
||||
views.push(view);
|
||||
return view;
|
||||
}
|
||||
const settle = () => vi.advanceTimersByTimeAsync(600);
|
||||
const last = () => published.filter(([, s]) => s === SYNTAX_ERRORS_PRODUCER).at(-1)?.[2];
|
||||
|
||||
afterEach(() => {
|
||||
views.splice(0).forEach((v) => v.destroy());
|
||||
published.length = 0;
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
describe("syntax errors without a language server", () => {
|
||||
it("reports the parser's errors when no server checks the language", async () => {
|
||||
vi.useFakeTimers();
|
||||
servers = [{ status: "missing", languages: ["php"] }];
|
||||
open();
|
||||
await settle();
|
||||
expect(last()).toEqual([
|
||||
expect.objectContaining({ severity: "error", message: "Unexpected token", source: "Syntax" }),
|
||||
expect.objectContaining({ severity: "error", message: "';' expected" }),
|
||||
]);
|
||||
});
|
||||
|
||||
it("leaves them to a running server, and checks again when the server stops", async () => {
|
||||
vi.useFakeTimers();
|
||||
servers = [{ status: "ready", languages: ["php"] }];
|
||||
serverListeners.forEach((l) => l());
|
||||
open();
|
||||
await settle();
|
||||
expect(last()).toEqual([]);
|
||||
servers = [{ status: "crashed", languages: ["php"] }];
|
||||
serverListeners.forEach((l) => l());
|
||||
await settle();
|
||||
expect(last()).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* Syntax errors from the built-in parser (`structure.syntaxErrors`) as diagnostics, for files
|
||||
* no language server checks: none is configured for the language, or it is not installed,
|
||||
* stopped or crashed. A running server reports syntax errors itself, so none are added then.
|
||||
*/
|
||||
import type { Extension } from "@codemirror/state";
|
||||
import { EditorView, ViewPlugin } from "@codemirror/view";
|
||||
import type { Diagnostic, ServerStatus, SyntaxError } from "@/bindings";
|
||||
import { whenSynced } from "@/editor/api";
|
||||
import { structure } from "@/editor/structure";
|
||||
import { tx } from "@/platform/i18n";
|
||||
import { SYNTAX_ERRORS_PRODUCER } from "@/platform/inspections";
|
||||
import { call, commands, isTauri, onIdeEvent } from "@/platform/ipc";
|
||||
import { setFileDiagnostics } from "./diagnostics";
|
||||
import { docOf } from "./lsp";
|
||||
|
||||
const INSPECT_DELAY = 500;
|
||||
/** Dialects the grammar doesn't follow; the database support checks SQL. */
|
||||
const SKIPPED = new Set(["sql", "plaintext"]);
|
||||
const RUNNING: ReadonlySet<ServerStatus> = new Set<ServerStatus>(["starting", "ready", "busy"]);
|
||||
|
||||
let checked: Promise<ReadonlySet<string>> | null = null;
|
||||
const listeners = new Set<() => void>();
|
||||
let offEvents: (() => void) | null = null;
|
||||
|
||||
/** Languages a running language server checks (cached until a server changes). */
|
||||
function checkedLanguages(): Promise<ReadonlySet<string>> {
|
||||
checked ??= call(commands.lspServers()).then(
|
||||
(servers) => new Set(servers.filter((s) => RUNNING.has(s.status)).flatMap((s) => s.languages)),
|
||||
() => new Set<string>(),
|
||||
);
|
||||
if (!offEvents) {
|
||||
const changed = () => {
|
||||
checked = null;
|
||||
for (const l of listeners) l();
|
||||
};
|
||||
const off = [onIdeEvent("languageServer", changed), onIdeEvent("languageServerRemoved", changed)];
|
||||
offEvents = () => off.forEach((f) => f());
|
||||
}
|
||||
return checked;
|
||||
}
|
||||
|
||||
function syntaxErrorDiagnostic(e: SyntaxError): Diagnostic {
|
||||
const expected = e.expected;
|
||||
return {
|
||||
range: e.range,
|
||||
severity: "error",
|
||||
message: e.kind === "missing" && expected ? tx`'${expected}' expected` : tx("Unexpected token"),
|
||||
source: tx("Syntax"),
|
||||
code: null,
|
||||
};
|
||||
}
|
||||
|
||||
/** Checks the file after edits (debounced) and when a language server starts or stops. */
|
||||
export function syntaxErrorsExtension(): Extension {
|
||||
return ViewPlugin.define((view: EditorView) => {
|
||||
const doc = docOf(view);
|
||||
if (!doc || doc.virtual || SKIPPED.has(doc.language) || !isTauri()) return {};
|
||||
let timer: ReturnType<typeof setTimeout> | undefined;
|
||||
let seq = 0;
|
||||
const run = async () => {
|
||||
const mine = ++seq;
|
||||
try {
|
||||
if ((await checkedLanguages()).has(doc.language)) {
|
||||
if (mine === seq) setFileDiagnostics(doc.path, SYNTAX_ERRORS_PRODUCER, []);
|
||||
return;
|
||||
}
|
||||
await whenSynced(view);
|
||||
const { errors } = await structure.syntaxErrors(doc.doc);
|
||||
if (mine !== seq) return;
|
||||
setFileDiagnostics(doc.path, SYNTAX_ERRORS_PRODUCER, errors.map(syntaxErrorDiagnostic));
|
||||
} catch (e) {
|
||||
console.debug("[syntax errors]", e);
|
||||
}
|
||||
};
|
||||
const schedule = () => {
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(() => void run(), INSPECT_DELAY);
|
||||
};
|
||||
schedule();
|
||||
listeners.add(schedule);
|
||||
return {
|
||||
update: (u) => {
|
||||
if (u.docChanged) schedule();
|
||||
},
|
||||
destroy: () => {
|
||||
clearTimeout(timer);
|
||||
listeners.delete(schedule);
|
||||
seq++;
|
||||
setFileDiagnostics(doc.path, SYNTAX_ERRORS_PRODUCER, []);
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -223,6 +223,9 @@ export interface BuiltinInspection {
|
||||
export const SPELLING_PRODUCER = "spelling";
|
||||
export const SPELLING_INSPECTION = "builtin:spelling";
|
||||
|
||||
/** Syntax errors of the built-in parser, for files no language server checks. */
|
||||
export const SYNTAX_ERRORS_PRODUCER = "syntax";
|
||||
|
||||
export const DEPENDENCY_PRODUCER = "dependency-versions";
|
||||
export const DEPENDENCY_INSPECTION = "builtin:dependency-versions";
|
||||
|
||||
@@ -256,6 +259,15 @@ export const BUILTIN_INSPECTIONS: readonly BuiltinInspection[] = [
|
||||
description: "Reports newer crate versions that Cargo.toml requirements do not already allow.",
|
||||
defaultLevel: "weakWarning",
|
||||
},
|
||||
{
|
||||
id: "builtin:syntax",
|
||||
producer: SYNTAX_ERRORS_PRODUCER,
|
||||
name: "Syntax errors",
|
||||
group: "General",
|
||||
description:
|
||||
"Reports code the built-in parser cannot read, in files no language server checks (none is configured for the language, or it is not installed, stopped or crashed). A running language server reports syntax errors itself.",
|
||||
defaultLevel: "error",
|
||||
},
|
||||
{
|
||||
id: "builtin:file-references",
|
||||
producer: "file-references",
|
||||
|
||||
@@ -5779,5 +5779,10 @@
|
||||
"File icon theme": "文件图标主题",
|
||||
"Plugin not enabled": "插件未启用",
|
||||
"Problems the {plugin} plugin reports. Severity \"As reported\" keeps the level the plugin assigns; a code below can override it.": "{plugin} 插件报告的问题。严重性“按报告”保留插件分配的级别;下方的代码可以覆盖它。",
|
||||
"The plugin system is a preview: its API and the plugins may still change.": "插件系统目前是预览版:API 和插件都可能还会变化。"
|
||||
"The plugin system is a preview: its API and the plugins may still change.": "插件系统目前是预览版:API 和插件都可能还会变化。",
|
||||
"'{expected}' expected": "缺少 '{expected}'",
|
||||
"Unexpected token": "意外的标记",
|
||||
"Syntax": "语法",
|
||||
"Syntax errors": "语法错误",
|
||||
"Reports code the built-in parser cannot read, in files no language server checks (none is configured for the language, or it is not installed, stopped or crashed). A running language server reports syntax errors itself.": "报告内置解析器无法解析的代码,仅针对没有语言服务器检查的文件(该语言未配置语言服务器,或未安装、已停止、已崩溃)。语言服务器运行时由它自己报告语法错误。"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user