дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 19:25:54 +02:00
Initial open-source release
Claude-Session: https://claude.ai/code/session_01B65qLEKnkeYTGdbwhLpjCu
Цей коміт міститься в:
коміт
9a2025b76e
2034 files changed
+243381
No files matched your search
@@ -0,0 +1,177 @@
|
||||
/**
|
||||
* Language support of query consoles beyond completion, answered from the
|
||||
* data source's schema model without a language server: unresolved-name
|
||||
* inspections, Go to Declaration (aliases and CTEs in the text, tables and
|
||||
* columns in the Database tool window), Quick Documentation and Reformat Code.
|
||||
*/
|
||||
import { EditorState, type Extension } from "@codemirror/state";
|
||||
import { ViewPlugin } from "@codemirror/view";
|
||||
import {
|
||||
CONTRIBUTED_DEFINITION,
|
||||
CONTRIBUTED_FORMAT,
|
||||
CONTRIBUTED_HOVER,
|
||||
type ContributedDefinition,
|
||||
type ContributedFormatter,
|
||||
type ContributedHover,
|
||||
} from "@/editor/api";
|
||||
import { setFileDiagnostics } from "@/editor/lsp/diagnostics";
|
||||
import { showHint } from "@/editor/lsp/hint";
|
||||
import { offsetToPos } from "@/editor/lsp/positions";
|
||||
import type { DbCompletion, DbCompletionTable, DbDriver } from "@/platform/ipc";
|
||||
import { formatSql } from "./sql/format";
|
||||
import { SqlResolver, type SqlSymbol } from "./sql/resolve";
|
||||
import { consoleSource, revealInTree, sourceById, useDatabase } from "./store";
|
||||
|
||||
/** Producer id of the console's diagnostics (Problems view, hover). */
|
||||
const INSPECTIONS = "sql-inspections";
|
||||
const INSPECT_DELAY = 400;
|
||||
|
||||
function sourceOf(path: string): { id: string | null; driver: DbDriver; model: DbCompletion | null } {
|
||||
const id = consoleSource(path);
|
||||
return {
|
||||
id,
|
||||
driver: sourceById(id)?.driver ?? "sqlite",
|
||||
model: id ? (useDatabase.getState().completion[id] ?? null) : null,
|
||||
};
|
||||
}
|
||||
|
||||
function resolver(path: string, state: EditorState): SqlResolver {
|
||||
const { driver, model } = sourceOf(path);
|
||||
return new SqlResolver(state.doc.toString(), driver, model);
|
||||
}
|
||||
|
||||
/** Unresolved tables / columns / aliases as error diagnostics, re-checked after edits and schema loads. */
|
||||
function inspections(path: string): Extension {
|
||||
return ViewPlugin.define((view) => {
|
||||
let timer: ReturnType<typeof setTimeout> | undefined;
|
||||
const run = () => {
|
||||
const doc = view.state.doc;
|
||||
const problems = sourceOf(path).model ? resolver(path, view.state).problems() : [];
|
||||
setFileDiagnostics(
|
||||
path,
|
||||
INSPECTIONS,
|
||||
problems.map((p) => ({
|
||||
range: { start: offsetToPos(doc, p.from), end: offsetToPos(doc, p.to) },
|
||||
severity: "error",
|
||||
message: p.message,
|
||||
source: "SQL",
|
||||
code: null,
|
||||
})),
|
||||
);
|
||||
};
|
||||
const schedule = () => {
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(run, INSPECT_DELAY);
|
||||
};
|
||||
schedule();
|
||||
// The schema model arrives (or the console is bound to another source) after the editor opens.
|
||||
const unsubscribe = useDatabase.subscribe((s, prev) => {
|
||||
if (s.completion !== prev.completion || s.consoles !== prev.consoles) schedule();
|
||||
});
|
||||
return {
|
||||
update: (u) => {
|
||||
if (u.docChanged) schedule();
|
||||
},
|
||||
destroy: () => {
|
||||
clearTimeout(timer);
|
||||
unsubscribe();
|
||||
setFileDiagnostics(path, INSPECTIONS, []);
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function definition(path: string): ContributedDefinition {
|
||||
return (view, pos) => {
|
||||
const r = resolver(path, view.state).resolveAt(pos);
|
||||
const sym = r?.symbol;
|
||||
const id = consoleSource(path);
|
||||
const reveal = (t: DbCompletionTable, column?: string) => {
|
||||
if (id) void revealInTree(id, t.schema, t.name, t.kind === "view", column);
|
||||
};
|
||||
if (!sym) {
|
||||
showHint(view, "Cannot find declaration to go to");
|
||||
return true;
|
||||
}
|
||||
switch (sym.kind) {
|
||||
case "alias":
|
||||
case "cte":
|
||||
// On the definition itself: its table, as IDEA does.
|
||||
if (pos >= sym.def.from && pos <= sym.def.to) {
|
||||
if (sym.kind === "alias" && sym.table) reveal(sym.table);
|
||||
else showHint(view, "Cannot find declaration to go to");
|
||||
} else view.dispatch({ selection: { anchor: sym.def.from, head: sym.def.to }, scrollIntoView: true });
|
||||
return true;
|
||||
case "table":
|
||||
reveal(sym.table);
|
||||
return true;
|
||||
case "column":
|
||||
if (sym.table) reveal(sym.table, sym.name);
|
||||
else showHint(view, "Cannot find declaration to go to");
|
||||
return true;
|
||||
case "schema":
|
||||
showHint(view, "Cannot find declaration to go to");
|
||||
return true;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const code = (s: string) => `\`${s.replace(/`/g, "")}\``;
|
||||
|
||||
function tableDoc(t: DbCompletionTable, model: DbCompletion | null): string {
|
||||
const fks = (model?.foreignKeys ?? []).filter((k) => k.schema === t.schema && k.table === t.name);
|
||||
const refOf = (c: string) => {
|
||||
const k = fks.find((f) => f.columns.includes(c));
|
||||
return k ? `→ ${k.refTable} (${k.refColumns[k.columns.indexOf(c)] ?? ""})` : "";
|
||||
};
|
||||
const rows = t.columns.map(
|
||||
(c) =>
|
||||
`| ${code(c.name)} | ${c.dataType} | ${[c.primaryKey ? "PK" : "", c.nullable ? "" : "not null", c.hasDefault ? "default" : "", refOf(c.name)]
|
||||
.filter(Boolean)
|
||||
.join(", ")} |`,
|
||||
);
|
||||
return [`${t.kind === "view" ? "view" : "table"} **${t.name}** in ${code(t.schema)}`, "", "| column | type | |", "|---|---|---|", ...rows].join(
|
||||
"\n",
|
||||
);
|
||||
}
|
||||
|
||||
function symbolDoc(sym: SqlSymbol, model: DbCompletion | null): string | null {
|
||||
switch (sym.kind) {
|
||||
case "table":
|
||||
return tableDoc(sym.table, model);
|
||||
case "alias":
|
||||
return sym.table ? `alias ${code(sym.name)} of\n\n${tableDoc(sym.table, model)}` : `alias ${code(sym.name)}`;
|
||||
case "cte":
|
||||
return `common table expression ${code(sym.name)}`;
|
||||
case "schema":
|
||||
return `schema ${code(sym.name)}`;
|
||||
case "column": {
|
||||
const c = sym.column;
|
||||
const owner = sym.table ? `${sym.table.schema}.${sym.table.name}` : sym.owner;
|
||||
if (!c) return `column ${code(sym.name)} of ${code(owner)}`;
|
||||
const traits = [c.primaryKey ? "primary key" : "", c.nullable ? "" : "not null", c.hasDefault ? "has default" : ""].filter(Boolean);
|
||||
return `column **${c.name}** ${code(c.dataType)}${traits.length ? ` ${traits.join(", ")}` : ""}\n\nof ${code(owner)}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function hover(path: string): ContributedHover {
|
||||
return (state, pos) => {
|
||||
const r = resolver(path, state).resolveAt(pos);
|
||||
const markdown = r?.symbol ? symbolDoc(r.symbol, sourceOf(path).model) : null;
|
||||
return r && markdown ? { from: r.from, to: r.to, markdown } : null;
|
||||
};
|
||||
}
|
||||
|
||||
function formatter(path: string): ContributedFormatter {
|
||||
return (state, range) => formatSql(state.doc.toString(), sourceOf(path).driver, range);
|
||||
}
|
||||
|
||||
export function sqlSupport(path: string): Extension {
|
||||
const data = {
|
||||
[CONTRIBUTED_DEFINITION]: definition(path),
|
||||
[CONTRIBUTED_HOVER]: hover(path),
|
||||
[CONTRIBUTED_FORMAT]: formatter(path),
|
||||
};
|
||||
return [inspections(path), EditorState.languageData.of(() => [data])];
|
||||
}
|
||||
Reference in new issue
Заблокувати користувача