Skip to content

Commit 68ecb20

Browse files
authored
feat: sqlCompletion helper to register all completion sources at once (#167)
## What Registering the SQL completion sources had gotten unwieldy — each of the three schema-aware sources required its own `dialect.language.data.of({ autocomplete })` call: ```ts dialect.language.data.of({ autocomplete: createCteCompletionSource({ parser }) }), dialect.language.data.of({ autocomplete: aliasColumnCompletionSource({ schema, parser, contextAnalyzer }) }), dialect.language.data.of({ autocomplete: unqualifiedColumnCompletionSource({ schema, parser, contextAnalyzer }) }), ``` This PR adds a convenience helper that does it in one call: ```ts sqlCompletion({ dialect, schema, parser, contextAnalyzer }), ``` ## Details - **New `sqlCompletion(config)`** (`src/sql/completion-extension.ts`) registers all three completion sources: - `createCteCompletionSource` — CTE names + their output columns - `aliasColumnCompletionSource` — `u.` → columns of `users` - `unqualifiedColumnCompletionSource` — `SELECT e` → `email` from FROM/JOIN tables - Shares a single parser and query-context analyzer across the sources so each edit is analyzed once. - Per-source toggles (`enableCteCompletion` / `enableAliasCompletion` / `enableColumnCompletion`, all default `true`), mirroring the `enable*` pattern in `sqlExtension`. - Exported from `src/index.ts` (`sqlCompletion`, `SqlCompletionConfig`). - Demo updated to use the helper. It's a separate helper from `sqlExtension` because completion sources must be registered against a specific dialect's `language`, which `sqlExtension` doesn't take. ## Testing - New `src/sql/__tests__/completion-extension.test.ts` verifies the helper registers all three sources by default and honors the enable/disable toggles. - Updated the exports snapshot in `src/__tests__/index.test.ts`. - `pnpm run typecheck`, `pnpm exec oxlint`, tests, and `pnpm run demo` build all pass. <!-- This is an auto-generated description by cubic. --> --- ## Summary by cubic Introduce `sqlCompletion({ dialect, schema, parser, contextAnalyzer })` to register CTE, alias-qualified, and unqualified column completions with one call. This reduces setup to a single extension and reuses one parser/context analyzer for better performance. - **New Features** - Added `sqlCompletion(config)` to register `createCteCompletionSource`, `aliasColumnCompletionSource`, and `unqualifiedColumnCompletionSource` against the provided dialect. - Shares a single parser and `QueryContextAnalyzer`; supports `enableCteCompletion`, `enableAliasCompletion`, and `enableColumnCompletion` toggles (default true). - Exported `sqlCompletion` and `SqlCompletionConfig`; updated demo to use the helper; added tests to verify defaults and toggles. <sup>Written for commit c67e464. Summary will update on new commits.</sup> <a href="https://cubic.dev/pr/marimo-team/codemirror-sql/pull/167?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. -->
1 parent 991e53a commit 68ecb20

5 files changed

Lines changed: 178 additions & 15 deletions

File tree

demo/index.ts

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,13 @@ import { Compartment, type EditorState, StateEffect, StateField } from "@codemir
44
import { keymap } from "@codemirror/view";
55
import { basicSetup, EditorView } from "codemirror";
66
import {
7-
aliasColumnCompletionSource,
8-
createCteCompletionSource,
97
DefaultSqlTooltipRenders,
108
defaultSqlHoverTheme,
119
NodeSqlParser,
1210
QueryContextAnalyzer,
1311
type SupportedDialects,
12+
sqlCompletion,
1413
sqlExtension,
15-
unqualifiedColumnCompletionSource,
1614
} from "../src/index.js";
1715
import { tableTooltipRenderer } from "./custom-renderers.js";
1816
import { defaultSqlDoc, schema } from "./data.js";
@@ -185,18 +183,11 @@ function initializeEditor() {
185183
parser,
186184
},
187185
}),
188-
defaultDialect.language.data.of({
189-
// Statement-scoped CTE names and their output columns
190-
autocomplete: createCteCompletionSource({ parser }),
191-
}),
192-
defaultDialect.language.data.of({
193-
// Complete `u.` -> columns of `users` in `SELECT ... FROM users u`
194-
autocomplete: aliasColumnCompletionSource({ schema, parser, contextAnalyzer }),
195-
}),
196-
defaultDialect.language.data.of({
197-
// Complete `SELECT e` -> `email` because `FROM users` is in the statement
198-
autocomplete: unqualifiedColumnCompletionSource({ schema, parser, contextAnalyzer }),
199-
}),
186+
// Register all schema-aware completion sources at once:
187+
// - CTE names and their output columns
188+
// - `u.` -> columns of `users` in `SELECT ... FROM users u`
189+
// - `SELECT e` -> `email` because `FROM users` is in the statement
190+
sqlCompletion({ dialect: defaultDialect, schema, parser, contextAnalyzer }),
200191
// Custom theme for better SQL editing
201192
EditorView.theme({
202193
"&": {

src/__tests__/index.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ describe("index.ts exports", () => {
2323
"gotoSqlDefinition",
2424
"renameSqlIdentifier",
2525
"resolveSqlSchema",
26+
"sqlCompletion",
2627
"sqlExtension",
2728
"sqlGotoDefinition",
2829
"sqlHighlightReferences",

src/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ export {
66
type ColumnCompletionConfig,
77
unqualifiedColumnCompletionSource,
88
} from "./sql/column-completion-source.js";
9+
export {
10+
type SqlCompletionConfig,
11+
sqlCompletion,
12+
} from "./sql/completion-extension.js";
913
export {
1014
createCteCompletionSource,
1115
type CteCompletionConfig,
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import type { CompletionSource } from "@codemirror/autocomplete";
2+
import { PostgreSQL, sql } from "@codemirror/lang-sql";
3+
import { EditorState } from "@codemirror/state";
4+
import { describe, expect, it } from "vitest";
5+
import { sqlCompletion } from "../completion-extension.js";
6+
7+
const schema = { users: ["id", "name", "email"] };
8+
9+
/** Collect the `autocomplete` completion sources registered on the language. */
10+
function autocompleteSources(...extensions: Parameters<typeof EditorState.create>[0]["extensions"][]) {
11+
const state = EditorState.create({
12+
doc: "SELECT ",
13+
extensions: [sql({ dialect: PostgreSQL, schema }), ...extensions],
14+
});
15+
return state
16+
.languageDataAt<CompletionSource>("autocomplete", state.doc.length)
17+
.filter((value) => typeof value === "function");
18+
}
19+
20+
describe("sqlCompletion", () => {
21+
it("registers all three completion sources by default", () => {
22+
// Baseline: lang-sql registers its own schema-based autocomplete source
23+
const baseline = autocompleteSources().length;
24+
const withHelper = autocompleteSources(
25+
sqlCompletion({ dialect: PostgreSQL, schema }),
26+
).length;
27+
expect(withHelper - baseline).toBe(3);
28+
});
29+
30+
it("omits sources that are disabled", () => {
31+
const baseline = autocompleteSources().length;
32+
const withHelper = autocompleteSources(
33+
sqlCompletion({
34+
dialect: PostgreSQL,
35+
schema,
36+
enableAliasCompletion: false,
37+
enableColumnCompletion: false,
38+
}),
39+
).length;
40+
expect(withHelper - baseline).toBe(1);
41+
});
42+
43+
it("returns no extra sources when everything is disabled", () => {
44+
const baseline = autocompleteSources().length;
45+
const withHelper = autocompleteSources(
46+
sqlCompletion({
47+
dialect: PostgreSQL,
48+
schema,
49+
enableCteCompletion: false,
50+
enableAliasCompletion: false,
51+
enableColumnCompletion: false,
52+
}),
53+
).length;
54+
expect(withHelper - baseline).toBe(0);
55+
});
56+
});

src/sql/completion-extension.ts

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import type { SQLDialect } from "@codemirror/lang-sql";
2+
import type { Extension } from "@codemirror/state";
3+
import { aliasColumnCompletionSource } from "./alias-completion-source.js";
4+
import { unqualifiedColumnCompletionSource } from "./column-completion-source.js";
5+
import { createCteCompletionSource } from "./cte-completion-source.js";
6+
import { NodeSqlParser } from "./parser.js";
7+
import { QueryContextAnalyzer } from "./query-context.js";
8+
import type { SqlSchemaSource } from "./schema-facet.js";
9+
import type { SqlParser } from "./types.js";
10+
11+
/**
12+
* Configuration for {@link sqlCompletion}, the convenience helper that
13+
* registers every schema-aware SQL completion source at once.
14+
*/
15+
export interface SqlCompletionConfig {
16+
/**
17+
* The SQL dialect whose language the completion sources are registered on
18+
* (e.g. `PostgreSQL`, or a dialect from `./dialects`). This must match the
19+
* dialect passed to `sql({ dialect })` for the sources to activate.
20+
*/
21+
dialect: SQLDialect;
22+
/**
23+
* Database schema to complete columns from. Falls back to the shared
24+
* `sqlSchemaFacet` when not provided. Not used by CTE completion, which
25+
* derives columns from the statement itself.
26+
*/
27+
schema?: SqlSchemaSource;
28+
/**
29+
* Custom SQL parser shared by all completion sources. Defaults to a new
30+
* `NodeSqlParser`. Pass the same instance used by the linter/hover so
31+
* dialect-specific setups only configure the parser once.
32+
*/
33+
parser?: SqlParser;
34+
/**
35+
* Query-context analyzer shared by all completion sources, so each edit is
36+
* analyzed once. Defaults to one built from `parser`.
37+
*/
38+
contextAnalyzer?: QueryContextAnalyzer;
39+
40+
/** Whether to enable CTE name/column completion (default: true) */
41+
enableCteCompletion?: boolean;
42+
/** Whether to enable alias-qualified column completion (default: true) */
43+
enableAliasCompletion?: boolean;
44+
/** Whether to enable unqualified column completion (default: true) */
45+
enableColumnCompletion?: boolean;
46+
}
47+
48+
/**
49+
* Registers every schema-aware SQL completion source in one call, so you don't
50+
* have to wire up each `dialect.language.data.of({ autocomplete })` by hand:
51+
* - {@link createCteCompletionSource} — CTE names and their output columns
52+
* - {@link aliasColumnCompletionSource} — `u.` → columns of `users` in
53+
* `SELECT ... FROM users u`
54+
* - {@link unqualifiedColumnCompletionSource} — `SELECT e` → `email` from the
55+
* statement's FROM/JOIN tables
56+
*
57+
* A single parser and query-context analyzer are shared across the sources so
58+
* each edit is analyzed only once. This complements `sqlExtension`, which
59+
* covers linting, hover, gutter, and navigation but not completion.
60+
*
61+
* @example
62+
* ```ts
63+
* import { sql, PostgreSQL } from '@codemirror/lang-sql';
64+
* import { sqlCompletion } from '@marimo-team/codemirror-sql';
65+
*
66+
* const schema = { users: ['id', 'name', 'email'] };
67+
* const extensions = [
68+
* sql({ dialect: PostgreSQL, schema }),
69+
* sqlCompletion({ dialect: PostgreSQL, schema }),
70+
* ];
71+
* ```
72+
*/
73+
export function sqlCompletion(config: SqlCompletionConfig): Extension[] {
74+
const {
75+
dialect,
76+
schema,
77+
parser = new NodeSqlParser(),
78+
enableCteCompletion = true,
79+
enableAliasCompletion = true,
80+
enableColumnCompletion = true,
81+
} = config;
82+
const contextAnalyzer = config.contextAnalyzer ?? new QueryContextAnalyzer(parser);
83+
84+
const extensions: Extension[] = [];
85+
86+
if (enableCteCompletion) {
87+
extensions.push(
88+
dialect.language.data.of({
89+
autocomplete: createCteCompletionSource({ parser, contextAnalyzer }),
90+
}),
91+
);
92+
}
93+
94+
if (enableAliasCompletion) {
95+
extensions.push(
96+
dialect.language.data.of({
97+
autocomplete: aliasColumnCompletionSource({ schema, parser, contextAnalyzer }),
98+
}),
99+
);
100+
}
101+
102+
if (enableColumnCompletion) {
103+
extensions.push(
104+
dialect.language.data.of({
105+
autocomplete: unqualifiedColumnCompletionSource({ schema, parser, contextAnalyzer }),
106+
}),
107+
);
108+
}
109+
110+
return extensions;
111+
}

0 commit comments

Comments
 (0)