@coldsmirk/inkstone-sql 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,31 @@
1
+ import { t as SqlAssist } from "./assist-gLTZKSD3.js";
2
+ import * as monacoEditor from "monaco-editor";
3
+ import { IDisposable } from "monaco-editor";
4
+
5
+ //#region src/monaco.d.ts
6
+ /**
7
+ * The monaco module itself, as `onMount` hands it over — typed structurally so this entry needs
8
+ * no dependency beyond `monaco-editor`'s own types.
9
+ */
10
+ type MonacoModule = typeof monacoEditor;
11
+ interface SqlLanguageHooks {
12
+ /**
13
+ * The assist as it stands right now — read per request rather than captured, so switching
14
+ * connections swaps the schema behind every feature without touching Monaco's registry.
15
+ */
16
+ assist: () => SqlAssist | null;
17
+ /**
18
+ * The dialect could not parse the statement, so nothing was reformatted. The host says so:
19
+ * a deliberate keystroke that changes nothing needs to explain itself.
20
+ */
21
+ onFormatRefused: () => void;
22
+ }
23
+ /**
24
+ * Register completion, hover, signature help and formatting for one model. The disposables are
25
+ * the caller's to hold: Monaco's registry outlives any editor, and a provider left behind would
26
+ * answer for a model that no longer exists. `languageId` is the language the model is registered
27
+ * under — the stock `"sql"` unless the host runs a dialect id of its own.
28
+ */
29
+ declare function registerSqlLanguage(monaco: MonacoModule, modelPath: string, hooks: SqlLanguageHooks, languageId?: string): IDisposable[];
30
+ //#endregion
31
+ export { MonacoModule, SqlLanguageHooks, registerSqlLanguage };
package/dist/monaco.js ADDED
@@ -0,0 +1,128 @@
1
+ //#region src/monaco.ts
2
+ function itemKinds(monaco) {
3
+ const kinds = monaco.languages.CompletionItemKind;
4
+ return {
5
+ keyword: kinds.Keyword,
6
+ function: kinds.Function,
7
+ schema: kinds.Module,
8
+ table: kinds.Struct,
9
+ view: kinds.Interface,
10
+ column: kinds.Field,
11
+ snippet: kinds.Snippet
12
+ };
13
+ }
14
+ const FORMATTING_KEY = Symbol.for("coldsmirk.inkstone.sql.formattingRegistry");
15
+ function formattingLedgers(monaco) {
16
+ const holder = globalThis;
17
+ const registries = holder[FORMATTING_KEY] ?? /* @__PURE__ */ new WeakMap();
18
+ holder[FORMATTING_KEY] = registries;
19
+ const existing = registries.get(monaco);
20
+ if (existing) return existing;
21
+ const created = /* @__PURE__ */ new Map();
22
+ registries.set(monaco, created);
23
+ return created;
24
+ }
25
+ function registerFormatting(monaco, languageId, modelPath, hooks) {
26
+ const ledgers = formattingLedgers(monaco);
27
+ let ledger = ledgers.get(languageId);
28
+ if (ledger === void 0) {
29
+ const byPath = /* @__PURE__ */ new Map();
30
+ ledger = {
31
+ hooks: byPath,
32
+ registration: monaco.languages.registerDocumentFormattingEditProvider(languageId, { provideDocumentFormattingEdits: async (model) => {
33
+ const owner = byPath.get(model.uri.toString());
34
+ const assist = owner?.assist() ?? null;
35
+ const text = model.getValue();
36
+ if (owner === void 0 || assist === null || text.trim() === "") return [];
37
+ const formatted = await assist.format(text);
38
+ if (formatted === null) {
39
+ owner.onFormatRefused();
40
+ return [];
41
+ }
42
+ return formatted === text ? [] : [{
43
+ range: model.getFullModelRange(),
44
+ text: formatted
45
+ }];
46
+ } })
47
+ };
48
+ ledgers.set(languageId, ledger);
49
+ }
50
+ const entered = ledger;
51
+ entered.hooks.set(modelPath, hooks);
52
+ return { dispose: () => {
53
+ entered.hooks.delete(modelPath);
54
+ if (entered.hooks.size === 0) {
55
+ entered.registration.dispose();
56
+ ledgers.delete(languageId);
57
+ }
58
+ } };
59
+ }
60
+ function registerSqlLanguage(monaco, modelPath, hooks, languageId = "sql") {
61
+ const kinds = itemKinds(monaco);
62
+ const path = monaco.Uri.parse(modelPath).toString();
63
+ const answering = (model) => model.uri.toString() === path ? hooks.assist() : null;
64
+ return [
65
+ monaco.languages.registerCompletionItemProvider(languageId, {
66
+ triggerCharacters: ["."],
67
+ provideCompletionItems: async (model, position) => {
68
+ const assist = answering(model);
69
+ if (assist === null) return { suggestions: [] };
70
+ const candidates = await assist.complete(model.getValue(), model.getOffsetAt(position));
71
+ const word = model.getWordUntilPosition(position);
72
+ return { suggestions: candidates.map((candidate, index) => {
73
+ return {
74
+ label: candidate.label,
75
+ kind: kinds[candidate.kind],
76
+ insertText: candidate.insertText,
77
+ insertTextRules: candidate.snippet ? monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet : void 0,
78
+ detail: candidate.detail,
79
+ documentation: candidate.documentation,
80
+ range: new monaco.Range(position.lineNumber, Math.max(1, word.startColumn - (candidate.replace ?? 0)), position.lineNumber, word.endColumn),
81
+ sortText: `${candidate.sortGroup}:${String(index).padStart(5, "0")}`
82
+ };
83
+ }) };
84
+ }
85
+ }),
86
+ monaco.languages.registerHoverProvider(languageId, { provideHover: async (model, position) => {
87
+ const assist = answering(model);
88
+ if (assist === null) return null;
89
+ const card = await assist.hover(model.getValue(), model.getOffsetAt(position));
90
+ if (card === null) return null;
91
+ const from = model.getPositionAt(card.from);
92
+ const to = model.getPositionAt(card.to);
93
+ return {
94
+ range: new monaco.Range(from.lineNumber, from.column, to.lineNumber, to.column),
95
+ contents: card.markdown.map((value) => {
96
+ return { value };
97
+ })
98
+ };
99
+ } }),
100
+ monaco.languages.registerSignatureHelpProvider(languageId, {
101
+ signatureHelpTriggerCharacters: ["(", ","],
102
+ signatureHelpRetriggerCharacters: [","],
103
+ provideSignatureHelp: async (model, position) => {
104
+ const assist = answering(model);
105
+ if (assist === null) return null;
106
+ const help = await assist.signature(model.getValue(), model.getOffsetAt(position));
107
+ if (help === null) return null;
108
+ return {
109
+ value: {
110
+ signatures: [{
111
+ label: help.label,
112
+ documentation: help.documentation,
113
+ parameters: help.parameters.map((span) => {
114
+ return { label: span };
115
+ })
116
+ }],
117
+ activeSignature: 0,
118
+ activeParameter: help.active
119
+ },
120
+ dispose: () => void 0
121
+ };
122
+ }
123
+ }),
124
+ registerFormatting(monaco, languageId, path, hooks)
125
+ ];
126
+ }
127
+ //#endregion
128
+ export { registerSqlLanguage };
@@ -0,0 +1,158 @@
1
+ import { t as SqlAssist } from "./assist-gLTZKSD3.js";
2
+ import { MonacoModule } from "./monaco.js";
3
+ import { ReactNode } from "react";
4
+ import { editor } from "monaco-editor";
5
+
6
+ //#region src/react.d.ts
7
+ /**
8
+ * What a host needs to reach into the editor for: inserting at the caret (a schema tree is an
9
+ * input device, not a browser), and reformatting from a toolbar — which goes through Monaco's own
10
+ * formatting action, so the button and `Shift+Alt+F` are one path and one undo.
11
+ */
12
+ interface SqlEditorHandle {
13
+ insert: (text: string) => void;
14
+ format: () => void;
15
+ }
16
+ /**
17
+ * Where the operator is in the document, in offsets — everything a host needs to decide which
18
+ * statement a run should send, and nothing about SQL. `from` and `to` are the selection's own
19
+ * bounds and are equal where nothing is selected: a caret is a selection of nothing.
20
+ *
21
+ * It rides with the text rather than beside it because the two must belong to the same instant.
22
+ * The controlled `value` is one React render behind the keystroke that triggered a run, and a
23
+ * console must run the statement the operator is looking at.
24
+ */
25
+ interface SqlCaret {
26
+ text: string;
27
+ from: number;
28
+ to: number;
29
+ }
30
+ interface SqlEditorProps {
31
+ value: string;
32
+ onChange: (value: string) => void;
33
+ /**
34
+ * Render with the dark theme. Like every inkstone editor, the component never reads the OS
35
+ * `prefers-color-scheme`; wire this to the app's color-scheme state.
36
+ *
37
+ * @default false
38
+ */
39
+ dark?: boolean;
40
+ /**
41
+ * Editor height in px, or `"fill"` to consume the parent's resolved height (the parent must be
42
+ * a `min-height: 0` flex item). Monaco's loading placeholder is `height: 100%`, so a height
43
+ * that resolves to nothing collapses the field to one line while the editor loads.
44
+ */
45
+ height?: number | "fill";
46
+ /**
47
+ * Editor font size in px. Omit to keep the inkstone default.
48
+ */
49
+ fontSize?: number;
50
+ /**
51
+ * UI locale for Monaco's built-in chrome — page-global and locked by the first editor to
52
+ * mount, exactly as on `<MonacoEditor>`.
53
+ */
54
+ locale?: "en" | "zh-cn";
55
+ placeholder?: string;
56
+ /**
57
+ * The editor's accessible name, set on Monaco's own input surface — several SQL editors on one
58
+ * page are indistinguishable to assistive tech without one.
59
+ */
60
+ ariaLabel: string;
61
+ /**
62
+ * The Monaco language id the model registers under.
63
+ *
64
+ * @default "sql"
65
+ */
66
+ languageId?: string;
67
+ /**
68
+ * Shown while the Monaco host loads / when it fails to load — passed through to
69
+ * `<MonacoEditor>`, whose English defaults apply when omitted.
70
+ */
71
+ loading?: ReactNode;
72
+ failure?: ReactNode;
73
+ /**
74
+ * Ctrl/Cmd-Enter — the run key, bound inside the editor because that is where the operator's
75
+ * hands are. It is handed the editor's live caret, from which the host works out what to send.
76
+ * Read through a ref at every press, so it always runs against the connection the *current*
77
+ * render is bound to.
78
+ */
79
+ onRun?: (caret: SqlCaret) => void;
80
+ /**
81
+ * The label the run action shows in Monaco's own chrome (command palette, context menu).
82
+ *
83
+ * @default "Run statement"
84
+ */
85
+ runActionLabel?: string;
86
+ /**
87
+ * The caret moved, or the text under it changed. The host tracks it to show what a run would
88
+ * send; the run key does not depend on that tracking having caught up.
89
+ */
90
+ onCaret?: (caret: SqlCaret) => void;
91
+ /**
92
+ * The span to mark as the one a run would send. Null leaves the gutter clean, which is the
93
+ * right answer while there is nothing to choose between.
94
+ */
95
+ highlight?: {
96
+ from: number;
97
+ to: number;
98
+ } | null;
99
+ /**
100
+ * The class the highlight's line decoration carries. The host owns its stylesheet — a
101
+ * left-margin bar reads well, because it says which statement without touching a single colour
102
+ * the syntax already assigned.
103
+ *
104
+ * @default "inkstone-sql-runnable"
105
+ */
106
+ highlightClassName?: string;
107
+ /**
108
+ * The dialect could not parse the statement, so the format action changed nothing. The host
109
+ * says so — a deliberate keystroke that changes nothing needs to explain itself.
110
+ */
111
+ onFormatRefused?: () => void;
112
+ onReady?: (handle: SqlEditorHandle) => void;
113
+ /**
114
+ * Runs after this component's own mount wiring, with the live editor and the monaco module —
115
+ * the escape hatch for host-specific concerns (focus shields, extra actions).
116
+ */
117
+ onMount?: (instance: editor.IStandaloneCodeEditor, monaco: MonacoModule) => void;
118
+ /**
119
+ * What the editor's language features answer from — the schema and dialect of the connection
120
+ * being edited (`createSqlAssist`). Read through a ref at every request, so the host can swap
121
+ * it without re-registering anything with Monaco.
122
+ */
123
+ assist?: SqlAssist;
124
+ }
125
+ /**
126
+ * The SQL editing surface: Monaco in `sql`, highlighted by the same shared Shiki grammar every
127
+ * other inkstone surface uses. It is deliberately not a script editor — a TypeScript language
128
+ * service and an ambient profile have no meaning for a vendor's SQL. Its language features are
129
+ * the host's: the `assist` knows the catalog and the dialect and this component only wires it to
130
+ * Monaco (`registerSqlLanguage`) — it never guesses at SQL itself.
131
+ *
132
+ * The editor draws no chrome of its own: it is a band of its host's surface, and the separation
133
+ * is the host's hairline rules — a border here would dress a pane up as a form input.
134
+ */
135
+ declare function SqlEditor({
136
+ value,
137
+ onChange,
138
+ dark,
139
+ height,
140
+ fontSize,
141
+ locale,
142
+ placeholder,
143
+ ariaLabel,
144
+ languageId,
145
+ loading,
146
+ failure,
147
+ onRun,
148
+ runActionLabel,
149
+ onCaret,
150
+ highlight,
151
+ highlightClassName,
152
+ onFormatRefused,
153
+ onReady,
154
+ onMount,
155
+ assist
156
+ }: SqlEditorProps): import("react").JSX.Element;
157
+ //#endregion
158
+ export { SqlCaret, SqlEditor, SqlEditorHandle, SqlEditorProps };
package/dist/react.js ADDED
@@ -0,0 +1,135 @@
1
+ import { registerSqlLanguage } from "./monaco.js";
2
+ import { MonacoEditor } from "@coldsmirk/inkstone-react/monaco";
3
+ import { useEffect, useInsertionEffect, useRef, useState } from "react";
4
+ import { jsx } from "react/jsx-runtime";
5
+ //#region src/react.tsx
6
+ const EDITOR_ID_KEY = Symbol.for("coldsmirk.inkstone.sql.editorId");
7
+ function nextEditorId() {
8
+ const holder = globalThis;
9
+ const id = holder[EDITOR_ID_KEY] ?? 0;
10
+ holder[EDITOR_ID_KEY] = id + 1;
11
+ return id;
12
+ }
13
+ const FORMAT_CHORD_CONTEXT_KEY = "inkstoneSqlFormatChord";
14
+ function useLatest(value) {
15
+ const ref = useRef(value);
16
+ useInsertionEffect(() => {
17
+ ref.current = value;
18
+ });
19
+ return ref;
20
+ }
21
+ function caretOf(instance) {
22
+ const model = instance.getModel();
23
+ const selection = instance.getSelection();
24
+ if (model === null || selection === null) return {
25
+ text: "",
26
+ from: 0,
27
+ to: 0
28
+ };
29
+ return {
30
+ text: model.getValue(),
31
+ from: model.getOffsetAt(selection.getStartPosition()),
32
+ to: model.getOffsetAt(selection.getEndPosition())
33
+ };
34
+ }
35
+ function monacoRange(model, span) {
36
+ const from = model.getPositionAt(span.from);
37
+ const to = model.getPositionAt(span.to);
38
+ return {
39
+ startLineNumber: from.lineNumber,
40
+ startColumn: from.column,
41
+ endLineNumber: to.lineNumber,
42
+ endColumn: to.column
43
+ };
44
+ }
45
+ function SqlEditor({ value, onChange, dark = false, height = 200, fontSize, locale, placeholder, ariaLabel, languageId = "sql", loading, failure, onRun, runActionLabel = "Run statement", onCaret, highlight = null, highlightClassName = "inkstone-sql-runnable", onFormatRefused, onReady, onMount, assist }) {
46
+ const [modelPath] = useState(() => `file:///inkstone-sql/${nextEditorId()}.sql`);
47
+ const bindings = useRef([]);
48
+ const assistRef = useLatest(assist ?? null);
49
+ const formatRefusedRef = useLatest(onFormatRefused);
50
+ const runRef = useLatest(onRun);
51
+ const caretChangedRef = useLatest(onCaret);
52
+ const readyRef = useLatest(onReady);
53
+ const mountRef = useLatest(onMount);
54
+ const instance = useRef(null);
55
+ const marked = useRef(null);
56
+ const [mounted, setMounted] = useState(false);
57
+ useEffect(() => {
58
+ const model = instance.current?.getModel();
59
+ if (model === null || model === void 0) return;
60
+ marked.current?.clear();
61
+ marked.current = highlight === null ? null : instance.current.createDecorationsCollection([{
62
+ range: monacoRange(model, highlight),
63
+ options: {
64
+ linesDecorationsClassName: highlightClassName,
65
+ isWholeLine: true
66
+ }
67
+ }]);
68
+ }, [
69
+ highlight,
70
+ highlightClassName,
71
+ mounted
72
+ ]);
73
+ useEffect(() => () => {
74
+ for (const binding of bindings.current) binding.dispose();
75
+ bindings.current = [];
76
+ }, []);
77
+ const fill = height === "fill";
78
+ return /* @__PURE__ */ jsx("div", {
79
+ style: fill ? {
80
+ height: "100%",
81
+ minHeight: 0
82
+ } : { height },
83
+ children: /* @__PURE__ */ jsx(MonacoEditor, {
84
+ showLineNumbers: true,
85
+ dark,
86
+ failure,
87
+ fontSize,
88
+ height: fill ? "100%" : height,
89
+ language: languageId,
90
+ loading,
91
+ locale,
92
+ path: modelPath,
93
+ placeholder,
94
+ value,
95
+ options: {
96
+ ariaLabel,
97
+ suggest: { snippetsPreventQuickSuggestions: false }
98
+ },
99
+ onChange,
100
+ onMount: (live, monaco) => {
101
+ instance.current = live;
102
+ live.createContextKey(FORMAT_CHORD_CONTEXT_KEY, true);
103
+ bindings.current.push(live.onDidChangeCursorSelection(() => caretChangedRef.current?.(caretOf(live))), live.addAction({
104
+ id: "inkstone-sql.run",
105
+ label: runActionLabel,
106
+ keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter],
107
+ run: (current) => runRef.current?.(caretOf(current))
108
+ }), monaco.editor.addKeybindingRule({
109
+ keybinding: monaco.KeyMod.Shift | monaco.KeyMod.Alt | monaco.KeyCode.KeyF,
110
+ command: "editor.action.formatDocument",
111
+ when: `${FORMAT_CHORD_CONTEXT_KEY} && editorTextFocus && !editorReadonly`
112
+ }), ...registerSqlLanguage(monaco, modelPath, {
113
+ assist: () => assistRef.current,
114
+ onFormatRefused: () => formatRefusedRef.current?.()
115
+ }, languageId));
116
+ readyRef.current?.({
117
+ format: () => void live.getAction("editor.action.formatDocument")?.run(),
118
+ insert: (text) => {
119
+ const selection = live.getSelection();
120
+ if (selection) live.executeEdits("inkstone-sql-insert", [{
121
+ range: selection,
122
+ text,
123
+ forceMoveMarkers: true
124
+ }]);
125
+ live.focus();
126
+ }
127
+ });
128
+ mountRef.current?.(live, monaco);
129
+ setMounted(true);
130
+ }
131
+ })
132
+ });
133
+ }
134
+ //#endregion
135
+ export { SqlEditor };
package/package.json ADDED
@@ -0,0 +1,81 @@
1
+ {
2
+ "name": "@coldsmirk/inkstone-sql",
3
+ "version": "0.13.0",
4
+ "description": "Schema-aware SQL editing intelligence for Monaco: lexical statement reading under per-dialect noise profiles, catalog-driven completion/hover/signature help, unified-placeholder awareness, formatting, and a drop-in React SQL editor.",
5
+ "keywords": [
6
+ "sql",
7
+ "monaco",
8
+ "completion",
9
+ "editor",
10
+ "autocomplete",
11
+ "dialect"
12
+ ],
13
+ "homepage": "https://github.com/coldsmirk/inkstone/tree/main/packages/sql#readme",
14
+ "bugs": "https://github.com/coldsmirk/inkstone/issues",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/coldsmirk/inkstone.git",
18
+ "directory": "packages/sql"
19
+ },
20
+ "license": "UNLICENSED",
21
+ "author": {
22
+ "name": "Venus"
23
+ },
24
+ "sideEffects": false,
25
+ "type": "module",
26
+ "exports": {
27
+ ".": {
28
+ "types": "./dist/index.d.ts",
29
+ "default": "./dist/index.js"
30
+ },
31
+ "./monaco": {
32
+ "types": "./dist/monaco.d.ts",
33
+ "default": "./dist/monaco.js"
34
+ },
35
+ "./react": {
36
+ "types": "./dist/react.d.ts",
37
+ "default": "./dist/react.js"
38
+ },
39
+ "./package.json": "./package.json"
40
+ },
41
+ "types": "./dist/index.d.ts",
42
+ "files": [
43
+ "dist"
44
+ ],
45
+ "dependencies": {
46
+ "sql-formatter": "^15.8.2"
47
+ },
48
+ "devDependencies": {
49
+ "monaco-editor": "^0.55.1",
50
+ "react": "^19.2.7",
51
+ "react-dom": "^19.2.7",
52
+ "@coldsmirk/inkstone-react": "^0.13.0"
53
+ },
54
+ "peerDependencies": {
55
+ "monaco-editor": "^0.55.1",
56
+ "react": ">=19",
57
+ "@coldsmirk/inkstone-react": "^0.13.0"
58
+ },
59
+ "peerDependenciesMeta": {
60
+ "@coldsmirk/inkstone-react": {
61
+ "optional": true
62
+ },
63
+ "monaco-editor": {
64
+ "optional": true
65
+ },
66
+ "react": {
67
+ "optional": true
68
+ }
69
+ },
70
+ "engines": {
71
+ "node": ">=24"
72
+ },
73
+ "publishConfig": {
74
+ "access": "public"
75
+ },
76
+ "scripts": {
77
+ "build": "tsdown",
78
+ "clean": "rimraf dist",
79
+ "typecheck": "tsc --noEmit"
80
+ }
81
+ }