sqllens 1.0.0 → 1.2.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.
- package/LICENSE +0 -10
- package/README.md +95 -85
- package/THIRD-PARTY-NOTICES.md +70 -5
- package/dist/api.d.ts +5 -3
- package/dist/api.js +17 -4
- package/dist/bigquery/behavior.d.ts +2 -0
- package/dist/bigquery/behavior.js +20 -0
- package/dist/bigquery/dot-path.d.ts +0 -2
- package/dist/bigquery/dot-path.js +0 -1
- package/dist/bigquery/fold.d.ts +8 -0
- package/dist/bigquery/fold.js +39 -0
- package/dist/bigquery/index.d.ts +7 -0
- package/dist/bigquery/index.js +10 -0
- package/dist/{infer/bigquery.d.ts → bigquery/infer.d.ts} +2 -2
- package/dist/{infer/bigquery.js → bigquery/infer.js} +4 -3
- package/dist/bigquery/lower.js +35 -12
- package/dist/bigquery/signatures.generated.d.ts +6 -0
- package/dist/bigquery/signatures.generated.js +1068 -0
- package/dist/completion/atn-walk.d.ts +13 -2
- package/dist/completion/atn-walk.js +13 -10
- package/dist/completion/complete.d.ts +4 -3
- package/dist/completion/complete.js +101 -45
- package/dist/completion/config.js +66 -0
- package/dist/completion/jinja-slot.d.ts +24 -0
- package/dist/completion/jinja-slot.js +126 -0
- package/dist/completion/parser-factory.d.ts +13 -1
- package/dist/completion/parser-factory.js +48 -0
- package/dist/databricks/behavior.d.ts +2 -0
- package/dist/databricks/behavior.js +19 -0
- package/dist/databricks/fold.d.ts +8 -0
- package/dist/databricks/fold.js +27 -0
- package/dist/databricks/index.d.ts +7 -0
- package/dist/databricks/index.js +10 -0
- package/dist/databricks/infer.d.ts +6 -0
- package/dist/databricks/infer.js +638 -0
- package/dist/databricks/signatures.generated.d.ts +6 -0
- package/dist/databricks/signatures.generated.js +1745 -0
- package/dist/derived-dialects.js +19 -1
- package/dist/dialect-behavior/behavior.d.ts +26 -0
- package/dist/dialect-behavior/behavior.js +1 -0
- package/dist/dialect-behavior/carrier.d.ts +5 -0
- package/dist/dialect-behavior/carrier.js +5 -0
- package/dist/dialect-behavior/coerce-rules.d.ts +7 -0
- package/dist/dialect-behavior/coerce-rules.js +69 -0
- package/dist/dialect-behavior/public-fold.d.ts +6 -0
- package/dist/dialect-behavior/public-fold.js +9 -0
- package/dist/dialect-behavior/registry.d.ts +6 -0
- package/dist/dialect-behavior/registry.js +34 -0
- package/dist/dialect-symbols.js +22 -17
- package/dist/dialect.d.ts +2 -2
- package/dist/document/document.d.ts +1 -1
- package/dist/document/document.js +8 -7
- package/dist/duckdb/behavior.d.ts +2 -0
- package/dist/duckdb/behavior.js +21 -0
- package/dist/duckdb/fold.d.ts +8 -0
- package/dist/duckdb/fold.js +29 -0
- package/dist/duckdb/index.d.ts +7 -0
- package/dist/duckdb/index.js +10 -0
- package/dist/{infer/duckdb.d.ts → duckdb/infer.d.ts} +2 -2
- package/dist/{infer/duckdb.js → duckdb/infer.js} +4 -3
- package/dist/duckdb/lower.js +47 -15
- package/dist/duckdb/signatures.generated.d.ts +6 -0
- package/dist/duckdb/signatures.generated.js +1072 -0
- package/dist/generated/bigquery/GoogleSQLParser.js +0 -7060
- package/dist/generated/databricks/DatabricksParser.js +0 -4800
- package/dist/generated/duckdb/DuckdbParser.js +0 -9100
- package/dist/generated/minijinja/MinijinjaParser.js +0 -380
- package/dist/generated/mysql/MysqlLexer.js +7357 -0
- package/dist/generated/mysql/MysqlParser.js +78520 -0
- package/dist/generated/postgres/PostgresParser.js +0 -8530
- package/dist/generated/redshift/RedshiftParser.js +0 -10970
- package/dist/generated/snowflake/SnowflakeParser.js +0 -7320
- package/dist/generated/sqlite/SqliteLexer.js +945 -0
- package/dist/generated/sqlite/SqliteParser.js +14682 -0
- package/dist/generated/trino/TrinoParser.js +0 -3770
- package/dist/generated/tsql/TSqlParser.js +0 -8420
- package/dist/ident/fold.d.ts +24 -15
- package/dist/ident/fold.js +12 -145
- package/dist/index.d.ts +6 -2
- package/dist/index.js +14 -8
- package/dist/infer/functions.d.ts +22 -11
- package/dist/infer/functions.js +26 -888
- package/dist/infer/infer.js +16 -14
- package/dist/infer/nullability.js +3 -4
- package/dist/infer/types.d.ts +1 -1
- package/dist/infer/types.js +7 -7
- package/dist/ir/ir.d.ts +26 -17
- package/dist/ir/part-span.d.ts +16 -1
- package/dist/ir/part-span.js +38 -10
- package/dist/ir/span.js +0 -2
- package/dist/ir/walk.js +3 -3
- package/dist/lineage/hops.js +13 -10
- package/dist/lineage/lineage.js +10 -7
- package/dist/minijinja/apply-tags.d.ts +14 -7
- package/dist/minijinja/apply-tags.js +66 -121
- package/dist/minijinja/parse.js +6 -6
- package/dist/minijinja/tag-ast.d.ts +29 -36
- package/dist/minijinja/tag-ast.js +210 -90
- package/dist/mysql/behavior.d.ts +2 -0
- package/dist/mysql/behavior.js +21 -0
- package/dist/mysql/fold.d.ts +8 -0
- package/dist/mysql/fold.js +49 -0
- package/dist/mysql/index.d.ts +7 -0
- package/dist/mysql/index.js +10 -0
- package/dist/mysql/infer.d.ts +20 -0
- package/dist/mysql/infer.js +156 -0
- package/dist/mysql/lower.d.ts +13 -0
- package/dist/mysql/lower.js +1443 -0
- package/dist/mysql/parse.d.ts +10 -0
- package/dist/mysql/parse.js +70 -0
- package/dist/mysql/signatures.generated.d.ts +6 -0
- package/dist/mysql/signatures.generated.js +508 -0
- package/dist/postgres/behavior.d.ts +2 -0
- package/dist/postgres/behavior.js +19 -0
- package/dist/postgres/fold.d.ts +8 -0
- package/dist/postgres/fold.js +30 -0
- package/dist/postgres/index.d.ts +7 -0
- package/dist/postgres/index.js +10 -0
- package/dist/{infer/postgres.d.ts → postgres/infer.d.ts} +2 -2
- package/dist/{infer/postgres.js → postgres/infer.js} +4 -3
- package/dist/postgres/lower.js +2 -2
- package/dist/postgres/signatures.generated.d.ts +6 -0
- package/dist/postgres/signatures.generated.js +2973 -0
- package/dist/qualify/check-calls.js +80 -134
- package/dist/qualify/qualify.js +16 -14
- package/dist/qualify/schema-provider.js +2 -2
- package/dist/qualify/schema.js +3 -3
- package/dist/qualify/template-provider.d.ts +45 -12
- package/dist/qualify/template-provider.js +69 -38
- package/dist/redshift/behavior.d.ts +2 -0
- package/dist/redshift/behavior.js +19 -0
- package/dist/redshift/fold.d.ts +8 -0
- package/dist/redshift/fold.js +31 -0
- package/dist/redshift/index.d.ts +7 -0
- package/dist/redshift/index.js +10 -0
- package/dist/{infer/redshift.d.ts → redshift/infer.d.ts} +2 -2
- package/dist/{infer/redshift.js → redshift/infer.js} +4 -3
- package/dist/redshift/lower.js +2 -2
- package/dist/redshift/signatures.generated.d.ts +6 -0
- package/dist/redshift/signatures.generated.js +757 -0
- package/dist/references/references.js +17 -12
- package/dist/scope/like-pattern.d.ts +2 -0
- package/dist/scope/like-pattern.js +15 -0
- package/dist/scope/scope.d.ts +5 -5
- package/dist/scope/scope.js +48 -42
- package/dist/sema/resolve.js +17 -12
- package/dist/session.d.ts +2 -2
- package/dist/signature/signature.d.ts +14 -6
- package/dist/signature/signature.js +30 -22
- package/dist/signature/signatures.d.ts +14 -12
- package/dist/signature/signatures.js +42 -582
- package/dist/snowflake/behavior.d.ts +2 -0
- package/dist/snowflake/behavior.js +22 -0
- package/dist/snowflake/fold.d.ts +8 -0
- package/dist/snowflake/fold.js +25 -0
- package/dist/snowflake/index.d.ts +7 -0
- package/dist/snowflake/index.js +10 -0
- package/dist/{infer/snowflake.d.ts → snowflake/infer.d.ts} +2 -2
- package/dist/{infer/snowflake.js → snowflake/infer.js} +4 -3
- package/dist/snowflake/lower.js +59 -19
- package/dist/snowflake/signatures.generated.d.ts +6 -0
- package/dist/snowflake/signatures.generated.js +2080 -0
- package/dist/sqlite/behavior.d.ts +2 -0
- package/dist/sqlite/behavior.js +19 -0
- package/dist/sqlite/fold.d.ts +8 -0
- package/dist/sqlite/fold.js +41 -0
- package/dist/sqlite/index.d.ts +7 -0
- package/dist/sqlite/index.js +10 -0
- package/dist/sqlite/infer.d.ts +12 -0
- package/dist/sqlite/infer.js +122 -0
- package/dist/sqlite/lower.d.ts +11 -0
- package/dist/sqlite/lower.js +1093 -0
- package/dist/sqlite/parse.d.ts +10 -0
- package/dist/sqlite/parse.js +70 -0
- package/dist/sqlite/signatures.generated.d.ts +6 -0
- package/dist/sqlite/signatures.generated.js +277 -0
- package/dist/symbols/symbols.js +14 -12
- package/dist/token/classify.js +31 -0
- package/dist/token/tokenize.js +4 -0
- package/dist/trino/behavior.d.ts +2 -0
- package/dist/trino/behavior.js +21 -0
- package/dist/trino/fold.d.ts +8 -0
- package/dist/trino/fold.js +39 -0
- package/dist/trino/index.d.ts +7 -0
- package/dist/trino/index.js +10 -0
- package/dist/{infer/trino.d.ts → trino/infer.d.ts} +2 -2
- package/dist/{infer/trino.js → trino/infer.js} +4 -3
- package/dist/trino/lower.js +5 -5
- package/dist/trino/signatures.generated.d.ts +6 -0
- package/dist/trino/signatures.generated.js +968 -0
- package/dist/tsql/behavior.d.ts +2 -0
- package/dist/tsql/behavior.js +20 -0
- package/dist/tsql/fold.d.ts +8 -0
- package/dist/tsql/fold.js +34 -0
- package/dist/tsql/index.d.ts +7 -0
- package/dist/tsql/index.js +10 -0
- package/dist/tsql/infer.d.ts +16 -0
- package/dist/tsql/infer.js +289 -0
- package/dist/tsql/lower.js +4 -4
- package/dist/tsql/signatures.generated.d.ts +6 -0
- package/dist/tsql/signatures.generated.js +640 -0
- package/package.json +15 -11
- package/dist/generated/bigquery/GoogleSQLLexer.d.ts +0 -407
- package/dist/generated/bigquery/GoogleSQLParser.d.ts +0 -9558
- package/dist/generated/bigquery/GoogleSQLParserListener.d.ts +0 -7777
- package/dist/generated/bigquery/GoogleSQLParserListener.js +0 -7070
- package/dist/generated/databricks/DatabricksLexer.d.ts +0 -566
- package/dist/generated/databricks/DatabricksParser.d.ts +0 -7771
- package/dist/generated/databricks/DatabricksParserListener.d.ts +0 -5737
- package/dist/generated/databricks/DatabricksParserListener.js +0 -5256
- package/dist/generated/duckdb/DuckdbLexer.d.ts +0 -691
- package/dist/generated/duckdb/DuckdbParser.d.ts +0 -13932
- package/dist/generated/duckdb/DuckdbParserListener.d.ts +0 -10049
- package/dist/generated/duckdb/DuckdbParserListener.js +0 -9138
- package/dist/generated/minijinja/MinijinjaLexer.d.ts +0 -108
- package/dist/generated/minijinja/MinijinjaParser.d.ts +0 -604
- package/dist/generated/minijinja/MinijinjaParserListener.d.ts +0 -449
- package/dist/generated/minijinja/MinijinjaParserListener.js +0 -410
- package/dist/generated/postgres/PostgresLexer.d.ts +0 -663
- package/dist/generated/postgres/PostgresParser.d.ts +0 -12963
- package/dist/generated/postgres/PostgresParserListener.d.ts +0 -9408
- package/dist/generated/postgres/PostgresParserListener.js +0 -8554
- package/dist/generated/redshift/RedshiftLexer.d.ts +0 -954
- package/dist/generated/redshift/RedshiftParser.d.ts +0 -16939
- package/dist/generated/redshift/RedshiftParserListener.d.ts +0 -12092
- package/dist/generated/redshift/RedshiftParserListener.js +0 -10994
- package/dist/generated/snowflake/SnowflakeLexer.d.ts +0 -1046
- package/dist/generated/snowflake/SnowflakeParser.d.ts +0 -14196
- package/dist/generated/snowflake/SnowflakeParserListener.d.ts +0 -8063
- package/dist/generated/snowflake/SnowflakeParserListener.js +0 -7330
- package/dist/generated/trino/TrinoLexer.d.ts +0 -381
- package/dist/generated/trino/TrinoParser.d.ts +0 -5340
- package/dist/generated/trino/TrinoParserListener.d.ts +0 -4704
- package/dist/generated/trino/TrinoParserListener.js +0 -4326
- package/dist/generated/tsql/TSqlLexer.d.ts +0 -1278
- package/dist/generated/tsql/TSqlParser.d.ts +0 -17267
- package/dist/generated/tsql/TSqlParserListener.d.ts +0 -9697
- package/dist/generated/tsql/TSqlParserListener.js +0 -8854
- package/dist/infer/dialect.d.ts +0 -21
- package/dist/infer/dialect.js +0 -74
- package/dist/infer/literals.d.ts +0 -6
- package/dist/infer/literals.js +0 -44
- package/dist/signature/generated/tsql.d.ts +0 -3
- package/dist/signature/generated/tsql.js +0 -260
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type ATN } from "antlr4ng";
|
|
2
2
|
/**
|
|
3
3
|
* What can legally come next at the caret:
|
|
4
4
|
* - `tokens`: candidate terminal token TYPES (keywords/punctuation/literals) collectable there.
|
|
@@ -9,6 +9,13 @@ export interface Candidates {
|
|
|
9
9
|
tokens: Set<number>;
|
|
10
10
|
rules: Set<number>;
|
|
11
11
|
}
|
|
12
|
+
/** The minimal token view the walk needs: each token's antlr type + channel, in source order. Both
|
|
13
|
+
* antlr's `Token` and our neutral `Token` (src/token/token.ts) satisfy it, so the walk runs over
|
|
14
|
+
* the document's OWN already-lexed token stream, never a re-parse. */
|
|
15
|
+
export interface WalkToken {
|
|
16
|
+
type: number;
|
|
17
|
+
channel: number;
|
|
18
|
+
}
|
|
12
19
|
/**
|
|
13
20
|
* Our own ATN candidate-collection walk — a reimplementation of antlr4-c3's
|
|
14
21
|
* `CodeCompletionCore` (`collectCandidates` / `processRule` / `translateStackToRuleIndex`),
|
|
@@ -20,5 +27,9 @@ export interface Candidates {
|
|
|
20
27
|
* (`tokenListIndex === caretListIndex`) every terminal transition's label contributes its token
|
|
21
28
|
* types as candidates — unless the current rule call stack is inside a preferred (name/column)
|
|
22
29
|
* rule, in which case we record that rule and suppress the raw tokens it subsumes.
|
|
30
|
+
*
|
|
31
|
+
* `atn` is the dialect's parser ATN (input-independent, a per-dialect static) and `tokens` is the
|
|
32
|
+
* document's own lexed token stream up to (at least) the caret; the walk consumes only their `type`
|
|
33
|
+
* and `channel`, so it reuses the already-parsed tokens rather than re-lexing the source.
|
|
23
34
|
*/
|
|
24
|
-
export declare function collectCandidates(
|
|
35
|
+
export declare function collectCandidates(atn: ATN, startRuleIndex: number, tokens: readonly WalkToken[], caretTokenIndex: number, preferredRules: Set<number>, ignoredTokens: Set<number>): Candidates;
|
|
@@ -10,13 +10,17 @@ import { AtomTransition, NotSetTransition, RangeTransition, RuleStopState, RuleT
|
|
|
10
10
|
* (`tokenListIndex === caretListIndex`) every terminal transition's label contributes its token
|
|
11
11
|
* types as candidates — unless the current rule call stack is inside a preferred (name/column)
|
|
12
12
|
* rule, in which case we record that rule and suppress the raw tokens it subsumes.
|
|
13
|
+
*
|
|
14
|
+
* `atn` is the dialect's parser ATN (input-independent, a per-dialect static) and `tokens` is the
|
|
15
|
+
* document's own lexed token stream up to (at least) the caret; the walk consumes only their `type`
|
|
16
|
+
* and `channel`, so it reuses the already-parsed tokens rather than re-lexing the source.
|
|
13
17
|
*/
|
|
14
|
-
export function collectCandidates(
|
|
15
|
-
const walk = new CandidateWalk(
|
|
18
|
+
export function collectCandidates(atn, startRuleIndex, tokens, caretTokenIndex, preferredRules, ignoredTokens) {
|
|
19
|
+
const walk = new CandidateWalk(atn, tokens, caretTokenIndex, preferredRules, ignoredTokens);
|
|
16
20
|
return walk.run(startRuleIndex);
|
|
17
21
|
}
|
|
18
22
|
class CandidateWalk {
|
|
19
|
-
|
|
23
|
+
atn;
|
|
20
24
|
preferredRules;
|
|
21
25
|
ignoredTokens;
|
|
22
26
|
tokens = { tokens: new Set(), rules: new Set() };
|
|
@@ -32,16 +36,15 @@ class CandidateWalk {
|
|
|
32
36
|
* during a walk — that persistence is what kills the blowup.
|
|
33
37
|
*/
|
|
34
38
|
shortcutMap = new Map();
|
|
35
|
-
constructor(
|
|
36
|
-
this.
|
|
39
|
+
constructor(atn, tokens, caretTokenIndex, preferredRules, ignoredTokens) {
|
|
40
|
+
this.atn = atn;
|
|
37
41
|
this.preferredRules = preferredRules;
|
|
38
42
|
this.ignoredTokens = ignoredTokens;
|
|
39
43
|
// Precompute the on-channel token types from 0 up to the caret token. The walk consumes
|
|
40
44
|
// these to prune paths that cannot match what the user already typed. Mirrors c3's
|
|
41
45
|
// `tokens` array built in `collectCandidates`.
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const tok = stream.get(i);
|
|
46
|
+
for (let i = 0; i <= caretTokenIndex && i < tokens.length; i++) {
|
|
47
|
+
const tok = tokens[i];
|
|
45
48
|
if (tok.channel !== Token.DEFAULT_CHANNEL)
|
|
46
49
|
continue;
|
|
47
50
|
this.inputTypes.push(tok.type);
|
|
@@ -52,7 +55,7 @@ class CandidateWalk {
|
|
|
52
55
|
this.caretListIndex = this.inputTypes.length - 1;
|
|
53
56
|
}
|
|
54
57
|
run(startRuleIndex) {
|
|
55
|
-
const startState = this.
|
|
58
|
+
const startState = this.atn.ruleToStartState[startRuleIndex];
|
|
56
59
|
if (startState)
|
|
57
60
|
this.processRule(startState, 0, []);
|
|
58
61
|
return this.tokens;
|
|
@@ -204,7 +207,7 @@ class CandidateWalk {
|
|
|
204
207
|
}
|
|
205
208
|
}
|
|
206
209
|
collectComplement(transition) {
|
|
207
|
-
const max = this.
|
|
210
|
+
const max = this.atn.maxTokenType;
|
|
208
211
|
// Cap the enumeration: a wide-open NotSet/Wildcard is not a useful keyword list.
|
|
209
212
|
const COMPLEMENT_CAP = 64;
|
|
210
213
|
const excluded = transition instanceof NotSetTransition && transition.label ? transition.label : null;
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import type { SqlDocument } from "../document/document.js";
|
|
2
2
|
import type { SchemaProvider } from "../qualify/schema-provider.js";
|
|
3
3
|
/** One completion candidate. The editor filters this list by the typed prefix and applies the
|
|
4
|
-
* chosen label at the caret; we only produce the labels, anchored at the caret offset.
|
|
4
|
+
* chosen label at the caret; we only produce the labels, anchored at the caret offset. The
|
|
5
|
+
* `"template"` kind is a host candidate for a jinja call slot (a dbt model for a ref's arg). */
|
|
5
6
|
export interface Completion {
|
|
6
7
|
label: string;
|
|
7
|
-
kind: "keyword" | "column" | "table" | "function";
|
|
8
|
-
/** Extra display info
|
|
8
|
+
kind: "keyword" | "column" | "table" | "function" | "template";
|
|
9
|
+
/** Extra display info, e.g. a column's type when the schema knows it. */
|
|
9
10
|
detail?: string;
|
|
10
11
|
}
|
|
11
12
|
/**
|
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
// completeAt() — scope-aware completion over a SqlDocument.
|
|
3
3
|
//
|
|
4
4
|
// The interactive editor feature that lives in the BROKEN-input world: the user
|
|
5
|
-
// is mid-keystroke, so this
|
|
6
|
-
//
|
|
7
|
-
//
|
|
5
|
+
// is mid-keystroke, so this drives an ATN candidate walk over the DOCUMENT'S OWN
|
|
6
|
+
// already-lexed token stream (cell.tokens, reused not re-parsed; the document's
|
|
7
|
+
// error-tolerant parse already ran, and for a templated document over the jinja
|
|
8
|
+
// placeholder), positions it at the caret, and turns the raw {tokens, rules} the
|
|
9
|
+
// walk reports into editor completion items:
|
|
8
10
|
// - keywords — candidate token types whose grammar literal is a word (FROM, …)
|
|
9
11
|
// - tables — schema table names, when the caret is at a relation-name slot
|
|
10
12
|
// - columns — the scope's visible columns, when at a value/column slot
|
|
@@ -15,11 +17,13 @@
|
|
|
15
17
|
// ---------------------------------------------------------------------------
|
|
16
18
|
import { Token } from "antlr4ng";
|
|
17
19
|
import { nodeAt } from "../document/node-at.js";
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
+
import { resolveBehavior } from "../dialect-behavior/registry.js";
|
|
21
|
+
import { DefaultTemplateProvider } from "../qualify/template-provider.js";
|
|
22
|
+
import { callOf } from "../minijinja/apply-tags.js";
|
|
20
23
|
import { collectCandidates } from "./atn-walk.js";
|
|
24
|
+
import { jinjaSlotAt } from "./jinja-slot.js";
|
|
21
25
|
import { COMPLETION_CONFIG } from "./config.js";
|
|
22
|
-
import {
|
|
26
|
+
import { completionMeta } from "./parser-factory.js";
|
|
23
27
|
/**
|
|
24
28
|
* Completion candidates for the caret at `offset` in `doc`. Schema-aware when a `Schema` is given
|
|
25
29
|
* (table names + column types). NEVER throws: on broken / mid-edit input it still returns the
|
|
@@ -38,24 +42,47 @@ export function completeAt(doc, offset, schema) {
|
|
|
38
42
|
export const complete = completeAt;
|
|
39
43
|
function collect(doc, offset, schema) {
|
|
40
44
|
const dialect = doc.dialect;
|
|
45
|
+
// Inside a jinja tag ({{ ref('| }}, {% if | %}, {{ a ~ | }}) the caret is not in SQL at all, so SQL
|
|
46
|
+
// completion is wrong: the tag was blanked to a placeholder sitting in some SQL slot, so the walk
|
|
47
|
+
// would otherwise offer keywords/tables/columns inside the jinja. A recognized call slot answers the
|
|
48
|
+
// host's candidates through the template provider (the neutral provider offers none); any other
|
|
49
|
+
// position strictly inside a tag answers nothing. Only a caret outside every tag falls through to
|
|
50
|
+
// ordinary SQL completion below. Tags are reused from the document, never re-parsed.
|
|
51
|
+
const tags = doc.templated?.tags;
|
|
52
|
+
if (tags) {
|
|
53
|
+
const slot = jinjaSlotAt(tags, doc.text, offset);
|
|
54
|
+
if (slot)
|
|
55
|
+
return templateCompletions(slot, schema);
|
|
56
|
+
if (tags.some((t) => offset > t.tagSpan.start && offset < t.tagSpan.end))
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
41
59
|
const cfg = COMPLETION_CONFIG[dialect];
|
|
42
|
-
// Route to the statement CELL owning the caret: the
|
|
43
|
-
// cell-relative caret) and the
|
|
44
|
-
//
|
|
45
|
-
//
|
|
60
|
+
// Route to the statement CELL owning the caret: the visible-column lookup runs over that cell's
|
|
61
|
+
// own scope tree (cell-relative caret) and the ATN walk over that cell's own tokens, so a caret
|
|
62
|
+
// in statement 2 of a multi-statement document completes through its real scope, not the compound
|
|
63
|
+
// facade. Single-cell: the cell IS the document, so this is identical to a whole-doc walk.
|
|
46
64
|
const cell = doc.cellAt(offset);
|
|
47
|
-
const cellText = cell ? cell.text : doc.text;
|
|
48
|
-
const cellOffset = cell ? offset - cell.span.start : offset;
|
|
49
65
|
const cellScopes = cell ? cell.scopes : doc.scopes;
|
|
50
66
|
const cellAst = cell ? cell.ast : doc.ast;
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
// the
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
67
|
+
// Two coordinate spaces: the scope/column lookup is CELL-relative (cell.scopes/cell.ast carry
|
|
68
|
+
// cell-relative spans), the token walk is DOCUMENT-relative (cell.tokens are shifted to doc
|
|
69
|
+
// coordinates), so `offset` drives the walk and `cellOffset` the scope lookup.
|
|
70
|
+
const cellOffset = cell ? offset - cell.span.start : offset;
|
|
71
|
+
// The ATN walk reuses the DOCUMENT'S OWN already-lexed token stream instead of re-parsing the
|
|
72
|
+
// text. For a TEMPLATED document those tokens are the SQL-over-placeholder stream (the jinja tags
|
|
73
|
+
// are channel-2 tokens the walk skips), so completion sees real SQL at document-true offsets and
|
|
74
|
+
// never has to re-derive the placeholder; the raw `{{ }}` text that made a fresh lexer die from
|
|
75
|
+
// char 0 is never handed to a lexer again. A synthetic EOF closes the stream (mapTokens drops
|
|
76
|
+
// antlr's EOF sentinel), matching the entry rule's EOF anchor; its `start` past every real token
|
|
77
|
+
// keeps it the caret-index fallback for an end-of-input caret.
|
|
78
|
+
const meta = completionMeta(dialect);
|
|
79
|
+
const end = cell ? cell.span.end : doc.text.length;
|
|
80
|
+
const walkTokens = [
|
|
81
|
+
...(cell ? cell.tokens : doc.tokens),
|
|
82
|
+
{ type: Token.EOF, channel: Token.DEFAULT_CHANNEL, start: end, text: "" },
|
|
83
|
+
];
|
|
84
|
+
const caretIdx = caretTokenIndex(walkTokens, offset);
|
|
85
|
+
const cand = collectCandidates(meta.atn, meta.entryRuleIndex, walkTokens, caretIdx, cfg.preferredRules, cfg.ignoredTokens);
|
|
59
86
|
const out = [];
|
|
60
87
|
const seen = new Set(); // dedup by `${kind}\0${label}`
|
|
61
88
|
const add = (c) => {
|
|
@@ -67,7 +94,7 @@ function collect(doc, offset, schema) {
|
|
|
67
94
|
};
|
|
68
95
|
// keywords — from candidate token types whose grammar literal is a word.
|
|
69
96
|
for (const type of cand.tokens) {
|
|
70
|
-
const label = keywordLabel(
|
|
97
|
+
const label = keywordLabel(meta.vocabulary, type);
|
|
71
98
|
if (label)
|
|
72
99
|
add({ label, kind: "keyword" });
|
|
73
100
|
}
|
|
@@ -85,20 +112,34 @@ function collect(doc, offset, schema) {
|
|
|
85
112
|
for (const c of visibleColumns(cellScopes, cellAst, dialect, cellOffset, schema))
|
|
86
113
|
add(c);
|
|
87
114
|
if (schema)
|
|
88
|
-
for (const c of fromRelationColumns(
|
|
115
|
+
for (const c of fromRelationColumns(walkTokens, cfg, schema, dialect, doc.templated?.tags, doc.text))
|
|
89
116
|
add(c);
|
|
90
117
|
}
|
|
91
118
|
// functions — value/column slot: the dialect's inference-registry function names.
|
|
92
119
|
if (atColumn) {
|
|
93
|
-
for (const fn of Object.keys(
|
|
120
|
+
for (const fn of Object.keys(resolveBehavior(dialect).functions))
|
|
94
121
|
add({ label: fn, kind: "function" });
|
|
95
122
|
}
|
|
96
123
|
return out;
|
|
97
124
|
}
|
|
125
|
+
/** The host's candidates for a jinja call slot, as completions. The template provider carries them,
|
|
126
|
+
* so this reads the `schema` when it is one (a DbtTemplateProvider IS a SchemaProvider, and the host
|
|
127
|
+
* already passes it here for column/table completion); the neutral provider offers none. A jinja slot
|
|
128
|
+
* with no candidates still returns [], never SQL keywords, so a caret inside a tag never leaks SQL
|
|
129
|
+
* completion. */
|
|
130
|
+
function templateCompletions(slot, schema) {
|
|
131
|
+
if (!(schema instanceof DefaultTemplateProvider))
|
|
132
|
+
return [];
|
|
133
|
+
return schema.templateCandidates(slot.callee, slot.argIndex, slot.packageName).map((c) => ({
|
|
134
|
+
label: c.label,
|
|
135
|
+
kind: "template",
|
|
136
|
+
...(c.detail !== undefined ? { detail: c.detail } : {}),
|
|
137
|
+
}));
|
|
138
|
+
}
|
|
98
139
|
/** The walk's caret token index: the first default-channel token whose `.start >= offset`; for an
|
|
99
|
-
* end-of-input caret that is the EOF
|
|
100
|
-
|
|
101
|
-
|
|
140
|
+
* end-of-input caret that is the EOF sentinel's index (last entry). Mirrors Task 10's tests' caret
|
|
141
|
+
* helper. `toks` is the document's own token stream (doc coordinates) with the EOF sentinel appended. */
|
|
142
|
+
function caretTokenIndex(toks, offset) {
|
|
102
143
|
for (let i = 0; i < toks.length; i++) {
|
|
103
144
|
const t = toks[i];
|
|
104
145
|
if (!t || t.channel !== Token.DEFAULT_CHANNEL)
|
|
@@ -111,8 +152,8 @@ function caretTokenIndex(m, offset) {
|
|
|
111
152
|
/** A candidate token type → a keyword label, or undefined if it is punctuation/operator or has no
|
|
112
153
|
* literal name. The grammar literal is single-quoted (`"'FROM'"`); strip the quotes and keep it
|
|
113
154
|
* only when it starts with a letter/underscore. */
|
|
114
|
-
function keywordLabel(
|
|
115
|
-
const literal =
|
|
155
|
+
function keywordLabel(vocabulary, type) {
|
|
156
|
+
const literal = vocabulary.getLiteralName(type);
|
|
116
157
|
if (!literal)
|
|
117
158
|
return undefined;
|
|
118
159
|
const unquoted = literal.startsWith("'") && literal.endsWith("'") ? literal.slice(1, -1) : literal;
|
|
@@ -127,33 +168,47 @@ function intersects(a, b) {
|
|
|
127
168
|
return false;
|
|
128
169
|
}
|
|
129
170
|
/**
|
|
130
|
-
* Broken-input FROM-relation fallback. The grammar reads a mid-edit `SELECT
|
|
171
|
+
* Broken-input FROM-relation fallback. The grammar reads a mid-edit `SELECT <caret> FROM t` as
|
|
131
172
|
* `SELECT FROM AS t` (FROM is a non-reserved identifier in Spark), so the document's scope has no
|
|
132
173
|
* `t` source and scope-based columns come back empty. To still offer the FROM relation's columns,
|
|
133
174
|
* scan the token stream for `<relationKeyword> <name>` (FROM/JOIN followed by an identifier) and
|
|
134
175
|
* surface those tables' schema columns. Token-driven, so it survives the mis-parse; gated by config
|
|
135
|
-
* token sets, so the core stays dialect-neutral.
|
|
176
|
+
* token sets, so the core stays dialect-neutral. A `{{ ref('orders') }}` FROM source blanks to a
|
|
177
|
+
* placeholder identifier, so that name token is resolved through the template provider first (see
|
|
178
|
+
* `columnsForName`), then the same schema lookup a plain table gets.
|
|
136
179
|
*/
|
|
137
|
-
function fromRelationColumns(
|
|
180
|
+
function fromRelationColumns(walkTokens, cfg, schema, dialect, tags, text) {
|
|
138
181
|
if (cfg.relationKeywordTokens.size === 0)
|
|
139
182
|
return [];
|
|
140
|
-
// Default-channel tokens only
|
|
141
|
-
const toks =
|
|
183
|
+
// Default-channel tokens only: hidden whitespace/comments sit between FROM and the name.
|
|
184
|
+
const toks = walkTokens.filter((t) => t.channel === Token.DEFAULT_CHANNEL);
|
|
142
185
|
const out = [];
|
|
186
|
+
const emit = (cols) => {
|
|
187
|
+
if (cols)
|
|
188
|
+
for (const c of cols)
|
|
189
|
+
out.push({ label: c.name, kind: "column", detail: c.type });
|
|
190
|
+
};
|
|
143
191
|
for (let i = 0; i + 1 < toks.length; i++) {
|
|
144
|
-
const
|
|
145
|
-
const
|
|
146
|
-
if (!
|
|
192
|
+
const kw = toks[i];
|
|
193
|
+
const next = toks[i + 1];
|
|
194
|
+
if (!kw || !next)
|
|
147
195
|
continue;
|
|
148
|
-
if (!cfg.relationKeywordTokens.has(
|
|
196
|
+
if (!cfg.relationKeywordTokens.has(kw.type))
|
|
149
197
|
continue;
|
|
150
|
-
|
|
198
|
+
// A templated source ({{ ref('orders') }}) blanks to a channel-2 tag the walk skips, so it sits
|
|
199
|
+
// in the gap between the relation keyword and the next SQL token (the alias, or the next clause).
|
|
200
|
+
// Resolve it through the provider: relationOf(call) -> name, then its columns come from the
|
|
201
|
+
// relation answer or the same schema.columnsFor a plain table gets. A plain schema / the neutral
|
|
202
|
+
// provider resolves nothing for it, so it contributes no fabricated columns.
|
|
203
|
+
const tag = tags?.find((t) => t.kind === "call" && t.tagSpan.start >= kw.start && t.tagSpan.start < next.start);
|
|
204
|
+
if (tag && schema instanceof DefaultTemplateProvider) {
|
|
205
|
+
const rel = schema.relationOf(callOf(tag, text));
|
|
206
|
+
emit(rel ? (rel.columns ?? schema.columnsFor(rel.nameParts, dialect)) : undefined);
|
|
151
207
|
continue;
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
out.push({ label: c.name, kind: "column", detail: c.type });
|
|
208
|
+
}
|
|
209
|
+
// Plain table: the next SQL token is the relation name.
|
|
210
|
+
if (cfg.nameTokens.has(next.type))
|
|
211
|
+
emit(schema.columnsFor([next.text ?? ""], dialect));
|
|
157
212
|
}
|
|
158
213
|
return out;
|
|
159
214
|
}
|
|
@@ -164,16 +219,17 @@ function visibleColumns(scopes, ast, dialect, offset, schema) {
|
|
|
164
219
|
const scope = enclosingScope(scopes, ast, offset);
|
|
165
220
|
if (!scope)
|
|
166
221
|
return [];
|
|
222
|
+
const behavior = resolveBehavior(dialect);
|
|
167
223
|
const out = [];
|
|
168
224
|
const seen = new Set();
|
|
169
225
|
for (const src of scope.sources.values()) {
|
|
170
226
|
for (const col of columnsOf(src, dialect, schema)) {
|
|
171
227
|
// Dedup by folded IDENTITY (quoted/unquoted twins collapse); labels render via displayName.
|
|
172
|
-
const key =
|
|
228
|
+
const key = behavior.fold(col.label);
|
|
173
229
|
if (seen.has(key))
|
|
174
230
|
continue;
|
|
175
231
|
seen.add(key);
|
|
176
|
-
out.push({ ...col, label: displayName(col.label
|
|
232
|
+
out.push({ ...col, label: behavior.displayName(col.label) });
|
|
177
233
|
}
|
|
178
234
|
}
|
|
179
235
|
return out;
|
|
@@ -15,6 +15,10 @@ import { DuckdbLexer } from "../generated/duckdb/DuckdbLexer.js";
|
|
|
15
15
|
import { DuckdbParser } from "../generated/duckdb/DuckdbParser.js";
|
|
16
16
|
import { TrinoLexer } from "../generated/trino/TrinoLexer.js";
|
|
17
17
|
import { TrinoParser } from "../generated/trino/TrinoParser.js";
|
|
18
|
+
import { SqliteLexer } from "../generated/sqlite/SqliteLexer.js";
|
|
19
|
+
import { SqliteParser } from "../generated/sqlite/SqliteParser.js";
|
|
20
|
+
import { MysqlLexer } from "../generated/mysql/MysqlLexer.js";
|
|
21
|
+
import { MysqlParser } from "../generated/mysql/MysqlParser.js";
|
|
18
22
|
// Databricks (Spark grammar) name-reference rules — each cited by its grammar rule:
|
|
19
23
|
// identifierReference → the table/view/name reference used in `relationPrimary` (post-FROM),
|
|
20
24
|
// `DatabricksParser.g4:755` (`IDENTIFIER(expr)` | multipartIdentifier).
|
|
@@ -116,6 +120,52 @@ const TRINO_NAME_TOKENS = new Set([
|
|
|
116
120
|
TrinoLexer.BACKQUOTED_IDENTIFIER,
|
|
117
121
|
TrinoLexer.DIGIT_IDENTIFIER,
|
|
118
122
|
]);
|
|
123
|
+
// ── SQLite (grammars-v4 fork) ───────────────────────────────────────────────
|
|
124
|
+
// post-FROM → table_name (the relation-name leaf; the enclosing `table_or_subquery` is a
|
|
125
|
+
// wider alternation that also recurses into `select_stmt` for a parenthesized
|
|
126
|
+
// subquery/join, so marking IT preferred would swallow completion inside a nested
|
|
127
|
+
// FROM (SELECT …) — table_name alone still fires at "FROM ‹›" with nothing typed,
|
|
128
|
+
// since the ATN walk explores entering it before any token is consumed. table_name
|
|
129
|
+
// is also the slot reused by INSERT INTO/UPDATE/ALTER/DROP/CREATE TABLE's table-name
|
|
130
|
+
// position, which is a bonus, not a target).
|
|
131
|
+
// SELECT/WHERE → expr (the value/column slot; expr_base's `column_name_excluding_string` and the
|
|
132
|
+
// qualified `table_name DOT column_name` form both nest under it, matching the
|
|
133
|
+
// Snowflake `expr` precedent — a single outer entry rule for the whole
|
|
134
|
+
// precedence-chain expression grammar).
|
|
135
|
+
// table_name ALSO appears inside expr_base's qualified-column-ref and `x IN table_name` forms; since
|
|
136
|
+
// expr is the outer frame there, those inner positions report columnRules only, not tableRules — a
|
|
137
|
+
// known, accepted imprecision (same shape as the other dialects' rule choices here).
|
|
138
|
+
const SQLITE_TABLE_RULES = new Set([SqliteParser.RULE_table_name]);
|
|
139
|
+
const SQLITE_COLUMN_RULES = new Set([SqliteParser.RULE_expr]);
|
|
140
|
+
const SQLITE_PREFERRED = new Set([...SQLITE_TABLE_RULES, ...SQLITE_COLUMN_RULES]);
|
|
141
|
+
const SQLITE_RELATION_KEYWORDS = new Set([SqliteLexer.FROM_, SqliteLexer.JOIN_]);
|
|
142
|
+
// SQLite's lexer folds plain/"double"/`backtick`/[bracket]-quoted names into ONE IDENTIFIER token
|
|
143
|
+
// (SqliteLexer.g4's IDENTIFIER rule matches all four forms), so there is no separate quoted-ident
|
|
144
|
+
// token type to add, unlike T-SQL/Trino/Postgres.
|
|
145
|
+
const SQLITE_NAME_TOKENS = new Set([SqliteLexer.IDENTIFIER]);
|
|
146
|
+
// ── MySQL (grammars-v4 mysql/Positive-Technologies fork) ───────────────────
|
|
147
|
+
// post-FROM → tableName (the relation-name leaf; the enclosing `tableSourceItem` is a wider
|
|
148
|
+
// 4-way alternation whose `subqueryTableItem` arm recurses into `selectStatement`
|
|
149
|
+
// for a parenthesized subquery, so marking IT preferred would swallow completion
|
|
150
|
+
// inside a nested "FROM (SELECT ... FROM ‹›)" — same table_or_subquery-vs-table_name
|
|
151
|
+
// trap as the SQLite entry above. tableName wraps fullId -> uid, so it still fires at
|
|
152
|
+
// "FROM ‹›" with nothing typed. tableName is also reused by INSERT INTO/UPDATE/DELETE/
|
|
153
|
+
// DDL's table-name slot, a bonus, not a target).
|
|
154
|
+
// SELECT/WHERE → expression (the outer frame of the expression -> predicate -> expressionAtom
|
|
155
|
+
// precedence chain; fullColumnName nests under it, matching the Snowflake/SQLite
|
|
156
|
+
// `expr`-as-single-outer-rule precedent).
|
|
157
|
+
// tableName ALSO appears inside fullColumnName-adjacent and IN-list positions reached from inside
|
|
158
|
+
// `expression`; since expression is the outer frame there, those inner positions report columnRules
|
|
159
|
+
// only, not tableRules — the same accepted imprecision as the other dialects' choices here.
|
|
160
|
+
const MYSQL_TABLE_RULES = new Set([MysqlParser.RULE_tableName]);
|
|
161
|
+
const MYSQL_COLUMN_RULES = new Set([MysqlParser.RULE_expression]);
|
|
162
|
+
const MYSQL_PREFERRED = new Set([...MYSQL_TABLE_RULES, ...MYSQL_COLUMN_RULES]);
|
|
163
|
+
const MYSQL_RELATION_KEYWORDS = new Set([MysqlLexer.FROM, MysqlLexer.JOIN]);
|
|
164
|
+
// MySQL's `uid` rule (the identifier slot fullId/tableName bottom out on) accepts simpleId (built on
|
|
165
|
+
// the plain ID token) or STRING_LITERAL — this fork's DOUBLE_QUOTE_ID/REVERSE_QUOTE_ID alternatives
|
|
166
|
+
// are commented out of `uid`, so backtick/double-quoted names lex to, and reach `uid` through,
|
|
167
|
+
// STRING_LITERAL (docs/identifier-delimiter-contract.md's MySQL note says the same).
|
|
168
|
+
const MYSQL_NAME_TOKENS = new Set([MysqlLexer.ID, MysqlLexer.STRING_LITERAL]);
|
|
119
169
|
export const COMPLETION_CONFIG = {
|
|
120
170
|
databricks: {
|
|
121
171
|
preferredRules: DATABRICKS_PREFERRED,
|
|
@@ -181,4 +231,20 @@ export const COMPLETION_CONFIG = {
|
|
|
181
231
|
relationKeywordTokens: TRINO_RELATION_KEYWORDS,
|
|
182
232
|
nameTokens: TRINO_NAME_TOKENS,
|
|
183
233
|
},
|
|
234
|
+
sqlite: {
|
|
235
|
+
preferredRules: SQLITE_PREFERRED,
|
|
236
|
+
ignoredTokens: new Set([Token.EOF]),
|
|
237
|
+
tableRules: SQLITE_TABLE_RULES,
|
|
238
|
+
columnRules: SQLITE_COLUMN_RULES,
|
|
239
|
+
relationKeywordTokens: SQLITE_RELATION_KEYWORDS,
|
|
240
|
+
nameTokens: SQLITE_NAME_TOKENS,
|
|
241
|
+
},
|
|
242
|
+
mysql: {
|
|
243
|
+
preferredRules: MYSQL_PREFERRED,
|
|
244
|
+
ignoredTokens: new Set([Token.EOF]),
|
|
245
|
+
tableRules: MYSQL_TABLE_RULES,
|
|
246
|
+
columnRules: MYSQL_COLUMN_RULES,
|
|
247
|
+
relationKeywordTokens: MYSQL_RELATION_KEYWORDS,
|
|
248
|
+
nameTokens: MYSQL_NAME_TOKENS,
|
|
249
|
+
},
|
|
184
250
|
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { TagNode } from "../minijinja/tag-ast.js";
|
|
2
|
+
/** Where the caret sits inside a jinja call tag. NEUTRAL, the callee is a bare string; the dbt
|
|
3
|
+
* meaning of the slot (ref arg0 = a model) is the consumer's to apply. */
|
|
4
|
+
export interface JinjaSlot {
|
|
5
|
+
/** The callee name, e.g. `"ref"`, `"source"`, `"my_macro"`. */
|
|
6
|
+
callee: string;
|
|
7
|
+
/** Dotted package before the callee (`dbt_utils` in `dbt_utils.star(...)`). */
|
|
8
|
+
packageName?: string;
|
|
9
|
+
/** 0-based index of the positional arg the caret is in. The callee-name slot (caret still in the
|
|
10
|
+
* callee identifier, `{{ my_mac|`) is `-1`. */
|
|
11
|
+
argIndex: number;
|
|
12
|
+
/** The already-typed text of this slot up to the caret, quote-stripped, the prefix a consumer
|
|
13
|
+
* filters its candidates by. Empty when the slot is untyped (`{{ ref(|`). */
|
|
14
|
+
prefix: string;
|
|
15
|
+
/** True when the slot is unclosed / mid-typing (an `incomplete` call node, or a bare callee with no
|
|
16
|
+
* open paren yet). */
|
|
17
|
+
incomplete: boolean;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The jinja completion slot at `offset`, or undefined when the caret is not in a completable jinja
|
|
21
|
+
* position. `tags` is `parseTemplated(...).tags` (or `doc.templated.tags`); `text` is the document
|
|
22
|
+
* source. Reuses the already-computed tags; never re-parses.
|
|
23
|
+
*/
|
|
24
|
+
export declare function jinjaSlotAt(tags: readonly TagNode[], text: string, offset: number): JinjaSlot | undefined;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// jinjaSlotAt(), where the caret sits inside a jinja tag, for completion.
|
|
3
|
+
//
|
|
4
|
+
// The NEUTRAL half of jinja completion (anvil REQ1/REQ2): given the templated
|
|
5
|
+
// document's tags + the caret offset, it says which call the caret is in and which
|
|
6
|
+
// arg slot, `{{ ref('cu| }}` -> { callee: "ref", argIndex: 0, prefix: "cu" }. It
|
|
7
|
+
// carries NO dbt vocabulary: it does not know that `ref`'s arg 0 is a model. A
|
|
8
|
+
// consumer (a DbtTemplateProvider / the host) maps callee + argIndex to a role (ref
|
|
9
|
+
// arg0 -> a model name) and supplies the candidates, exactly the way the SQL side
|
|
10
|
+
// maps a grammar slot to a schema lookup.
|
|
11
|
+
//
|
|
12
|
+
// It finds the INNERMOST call covering the caret across every tag's call list, so a
|
|
13
|
+
// nested `outer(inner(|))` reports `inner`, and a call embedded in a control tag
|
|
14
|
+
// (`{% if is_incremental(| %}`) is found at all, not just a top-level `{{ call() }}`.
|
|
15
|
+
// A caret on a bare leading identifier with no open paren yet (`{{ re`, still typing
|
|
16
|
+
// the callee) is a callee-name slot too, read straight off the text.
|
|
17
|
+
//
|
|
18
|
+
// Reuses the parse: it reads the tags the document already produced, never re-parses.
|
|
19
|
+
// Total: returns undefined off any jinja completion slot; never throws.
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
/**
|
|
22
|
+
* The jinja completion slot at `offset`, or undefined when the caret is not in a completable jinja
|
|
23
|
+
* position. `tags` is `parseTemplated(...).tags` (or `doc.templated.tags`); `text` is the document
|
|
24
|
+
* source. Reuses the already-computed tags; never re-parses.
|
|
25
|
+
*/
|
|
26
|
+
export function jinjaSlotAt(tags, text, offset) {
|
|
27
|
+
const hit = innermostCallAt(tags, offset);
|
|
28
|
+
if (hit)
|
|
29
|
+
return slotFromCall(hit, text, offset);
|
|
30
|
+
// No call covers the caret: a bare leading identifier being typed is still a callee-name slot.
|
|
31
|
+
return bareCalleeSlot(tags, text, offset);
|
|
32
|
+
}
|
|
33
|
+
/** The innermost call covering `offset`: the top-level call node of a `{{ call() }}` tag, plus every
|
|
34
|
+
* nested call (`calls[1..]`) and every call embedded in a `{% … %}` control tag (`calls[]`). The
|
|
35
|
+
* smallest covering extent wins, so `inner` beats `outer` and a control-tag call is reachable. */
|
|
36
|
+
function innermostCallAt(tags, offset) {
|
|
37
|
+
let best;
|
|
38
|
+
const consider = (h) => {
|
|
39
|
+
if (offset < h.start || offset > h.end)
|
|
40
|
+
return;
|
|
41
|
+
if (!best || h.end - h.start < best.end - best.start)
|
|
42
|
+
best = h;
|
|
43
|
+
};
|
|
44
|
+
for (const t of tags) {
|
|
45
|
+
if (t.kind === "call") {
|
|
46
|
+
// The node's own top-level call carries the `incomplete` flag; its tag span is the extent so a
|
|
47
|
+
// caret in the tag's leading whitespace still resolves to it. `calls[0]` duplicates this
|
|
48
|
+
// top-level, so nested calls are `calls[1..]`.
|
|
49
|
+
consider({
|
|
50
|
+
name: t.name,
|
|
51
|
+
nameSpan: t.nameSpan,
|
|
52
|
+
...(t.packageName !== undefined ? { packageName: t.packageName } : {}),
|
|
53
|
+
...(t.argsSpan ? { argsSpan: t.argsSpan } : {}),
|
|
54
|
+
args: t.args,
|
|
55
|
+
incomplete: t.incomplete === true,
|
|
56
|
+
start: t.tagSpan.start,
|
|
57
|
+
end: t.tagSpan.end,
|
|
58
|
+
});
|
|
59
|
+
for (const c of t.calls.slice(1))
|
|
60
|
+
consider(macroHit(c));
|
|
61
|
+
}
|
|
62
|
+
else if (t.kind === "control") {
|
|
63
|
+
for (const c of t.calls)
|
|
64
|
+
consider(macroHit(c));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return best;
|
|
68
|
+
}
|
|
69
|
+
/** A nested / control-embedded MacroCall as a CallHit: its extent is the callee (with any package)
|
|
70
|
+
* through the close paren, so it is tighter than the enclosing tag and wins the innermost pick. */
|
|
71
|
+
function macroHit(c) {
|
|
72
|
+
return {
|
|
73
|
+
name: c.name,
|
|
74
|
+
nameSpan: c.nameSpan,
|
|
75
|
+
...(c.packageName !== undefined ? { packageName: c.packageName } : {}),
|
|
76
|
+
...(c.argsSpan ? { argsSpan: c.argsSpan } : {}),
|
|
77
|
+
args: c.args,
|
|
78
|
+
incomplete: false,
|
|
79
|
+
start: c.packageSpan?.start ?? c.nameSpan.start,
|
|
80
|
+
end: c.argsSpan?.end ?? c.nameSpan.end,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
/** The slot for a caret inside a resolved call: the callee name, or the positional argument. */
|
|
84
|
+
function slotFromCall(c, text, offset) {
|
|
85
|
+
const base = { callee: c.name, ...(c.packageName !== undefined ? { packageName: c.packageName } : {}) };
|
|
86
|
+
// Callee-name slot: the caret is still within (or right at the end of) the callee identifier,
|
|
87
|
+
// before the open paren, the user is typing the macro name itself.
|
|
88
|
+
if (offset <= c.nameSpan.end) {
|
|
89
|
+
return { ...base, argIndex: -1, prefix: text.slice(c.nameSpan.start, offset), incomplete: c.incomplete };
|
|
90
|
+
}
|
|
91
|
+
// Between the name and the open paren (e.g. whitespace) is no completable slot.
|
|
92
|
+
const parenStart = c.argsSpan?.start ?? Number.MAX_SAFE_INTEGER;
|
|
93
|
+
if (offset < parenStart)
|
|
94
|
+
return undefined;
|
|
95
|
+
// Inside the arguments. The arg whose span covers the caret; else the caret sits in a gap (after
|
|
96
|
+
// the open paren or a comma), so the slot is the next arg being typed = the count of args that
|
|
97
|
+
// already ended before the caret.
|
|
98
|
+
const inArg = c.args.findIndex((a) => offset >= a.span.start && offset <= a.span.end);
|
|
99
|
+
if (inArg >= 0) {
|
|
100
|
+
return { ...base, argIndex: inArg, prefix: stripQuote(text.slice(c.args[inArg].span.start, offset)), incomplete: c.incomplete };
|
|
101
|
+
}
|
|
102
|
+
const argIndex = c.args.filter((a) => a.span.end <= offset).length;
|
|
103
|
+
return { ...base, argIndex, prefix: "", incomplete: c.incomplete };
|
|
104
|
+
}
|
|
105
|
+
/** A bare leading identifier being typed in a `{{ }}` expression (`{{ re`, `{{ region`) as a
|
|
106
|
+
* callee-name slot. The `other` tag drops the identifier, so read it off the text: only an
|
|
107
|
+
* expression tag (opens `{{`), and only when the caret sits on a single leading identifier (nothing
|
|
108
|
+
* but whitespace before it, no member access or operators). So a callee being typed toward a call
|
|
109
|
+
* and a bare variable both offer the host's callee candidates, filtered by the prefix. */
|
|
110
|
+
function bareCalleeSlot(tags, text, offset) {
|
|
111
|
+
const tag = tags.find((t) => t.kind === "other" && offset > t.tagSpan.start && offset <= t.tagSpan.end);
|
|
112
|
+
if (!tag)
|
|
113
|
+
return undefined;
|
|
114
|
+
if (text.slice(tag.tagSpan.start, tag.tagSpan.start + 2) !== "{{")
|
|
115
|
+
return undefined;
|
|
116
|
+
const before = text.slice(tag.tagSpan.start + 2, offset);
|
|
117
|
+
const m = /^\s*([A-Za-z_]\w*)$/.exec(before);
|
|
118
|
+
if (!m)
|
|
119
|
+
return undefined;
|
|
120
|
+
return { callee: m[1], argIndex: -1, prefix: m[1], incomplete: true };
|
|
121
|
+
}
|
|
122
|
+
/** Drop a single leading quote from a partial string arg (`'cu` -> `cu`) so the prefix is the value
|
|
123
|
+
* the consumer filters by. Leaves a non-string arg untouched. */
|
|
124
|
+
function stripQuote(raw) {
|
|
125
|
+
return raw.replace(/^['"]/, "");
|
|
126
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CommonTokenStream, type Lexer, type Parser, type ParserRuleContext } from "antlr4ng";
|
|
1
|
+
import { type ATN, CommonTokenStream, type Lexer, type Parser, type ParserRuleContext, type Vocabulary } from "antlr4ng";
|
|
2
2
|
import type { Dialect } from "../dialect.js";
|
|
3
3
|
/**
|
|
4
4
|
* A ready-to-walk parser for the completion engine: the lexer, the token stream, the entry
|
|
@@ -21,3 +21,15 @@ export interface MadeParser {
|
|
|
21
21
|
}
|
|
22
22
|
/** Build a fresh error-tolerant parser for `dialect`, lexing `sql`. */
|
|
23
23
|
export declare function makeParser(sql: string, dialect: Dialect): MadeParser;
|
|
24
|
+
/** The input-INDEPENDENT parser facts the ATN candidate walk needs: the dialect's parser ATN, the
|
|
25
|
+
* lexer vocabulary (for keyword literal labels), and the batch entry rule's index. All three are
|
|
26
|
+
* per-dialect statics (the ATN and vocabulary are shared across every parser/lexer instance), so
|
|
27
|
+
* they are grabbed once from a throwaway empty-input factory and reused; no source is re-lexed. */
|
|
28
|
+
export interface CompletionMeta {
|
|
29
|
+
atn: ATN;
|
|
30
|
+
vocabulary: Vocabulary;
|
|
31
|
+
entryRuleIndex: number;
|
|
32
|
+
}
|
|
33
|
+
/** The cached {@link CompletionMeta} for `dialect`, built once. Completion drives the walk over the
|
|
34
|
+
* document's own token stream plus this meta, instead of re-parsing the source text. */
|
|
35
|
+
export declare function completionMeta(dialect: Dialect): CompletionMeta;
|