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.
Files changed (244) hide show
  1. package/LICENSE +0 -10
  2. package/README.md +95 -85
  3. package/THIRD-PARTY-NOTICES.md +70 -5
  4. package/dist/api.d.ts +5 -3
  5. package/dist/api.js +17 -4
  6. package/dist/bigquery/behavior.d.ts +2 -0
  7. package/dist/bigquery/behavior.js +20 -0
  8. package/dist/bigquery/dot-path.d.ts +0 -2
  9. package/dist/bigquery/dot-path.js +0 -1
  10. package/dist/bigquery/fold.d.ts +8 -0
  11. package/dist/bigquery/fold.js +39 -0
  12. package/dist/bigquery/index.d.ts +7 -0
  13. package/dist/bigquery/index.js +10 -0
  14. package/dist/{infer/bigquery.d.ts → bigquery/infer.d.ts} +2 -2
  15. package/dist/{infer/bigquery.js → bigquery/infer.js} +4 -3
  16. package/dist/bigquery/lower.js +35 -12
  17. package/dist/bigquery/signatures.generated.d.ts +6 -0
  18. package/dist/bigquery/signatures.generated.js +1068 -0
  19. package/dist/completion/atn-walk.d.ts +13 -2
  20. package/dist/completion/atn-walk.js +13 -10
  21. package/dist/completion/complete.d.ts +4 -3
  22. package/dist/completion/complete.js +101 -45
  23. package/dist/completion/config.js +66 -0
  24. package/dist/completion/jinja-slot.d.ts +24 -0
  25. package/dist/completion/jinja-slot.js +126 -0
  26. package/dist/completion/parser-factory.d.ts +13 -1
  27. package/dist/completion/parser-factory.js +48 -0
  28. package/dist/databricks/behavior.d.ts +2 -0
  29. package/dist/databricks/behavior.js +19 -0
  30. package/dist/databricks/fold.d.ts +8 -0
  31. package/dist/databricks/fold.js +27 -0
  32. package/dist/databricks/index.d.ts +7 -0
  33. package/dist/databricks/index.js +10 -0
  34. package/dist/databricks/infer.d.ts +6 -0
  35. package/dist/databricks/infer.js +638 -0
  36. package/dist/databricks/signatures.generated.d.ts +6 -0
  37. package/dist/databricks/signatures.generated.js +1745 -0
  38. package/dist/derived-dialects.js +19 -1
  39. package/dist/dialect-behavior/behavior.d.ts +26 -0
  40. package/dist/dialect-behavior/behavior.js +1 -0
  41. package/dist/dialect-behavior/carrier.d.ts +5 -0
  42. package/dist/dialect-behavior/carrier.js +5 -0
  43. package/dist/dialect-behavior/coerce-rules.d.ts +7 -0
  44. package/dist/dialect-behavior/coerce-rules.js +69 -0
  45. package/dist/dialect-behavior/public-fold.d.ts +6 -0
  46. package/dist/dialect-behavior/public-fold.js +9 -0
  47. package/dist/dialect-behavior/registry.d.ts +6 -0
  48. package/dist/dialect-behavior/registry.js +34 -0
  49. package/dist/dialect-symbols.js +22 -17
  50. package/dist/dialect.d.ts +2 -2
  51. package/dist/document/document.d.ts +1 -1
  52. package/dist/document/document.js +8 -7
  53. package/dist/duckdb/behavior.d.ts +2 -0
  54. package/dist/duckdb/behavior.js +21 -0
  55. package/dist/duckdb/fold.d.ts +8 -0
  56. package/dist/duckdb/fold.js +29 -0
  57. package/dist/duckdb/index.d.ts +7 -0
  58. package/dist/duckdb/index.js +10 -0
  59. package/dist/{infer/duckdb.d.ts → duckdb/infer.d.ts} +2 -2
  60. package/dist/{infer/duckdb.js → duckdb/infer.js} +4 -3
  61. package/dist/duckdb/lower.js +47 -15
  62. package/dist/duckdb/signatures.generated.d.ts +6 -0
  63. package/dist/duckdb/signatures.generated.js +1072 -0
  64. package/dist/generated/bigquery/GoogleSQLParser.js +0 -7060
  65. package/dist/generated/databricks/DatabricksParser.js +0 -4800
  66. package/dist/generated/duckdb/DuckdbParser.js +0 -9100
  67. package/dist/generated/minijinja/MinijinjaParser.js +0 -380
  68. package/dist/generated/mysql/MysqlLexer.js +7357 -0
  69. package/dist/generated/mysql/MysqlParser.js +78520 -0
  70. package/dist/generated/postgres/PostgresParser.js +0 -8530
  71. package/dist/generated/redshift/RedshiftParser.js +0 -10970
  72. package/dist/generated/snowflake/SnowflakeParser.js +0 -7320
  73. package/dist/generated/sqlite/SqliteLexer.js +945 -0
  74. package/dist/generated/sqlite/SqliteParser.js +14682 -0
  75. package/dist/generated/trino/TrinoParser.js +0 -3770
  76. package/dist/generated/tsql/TSqlParser.js +0 -8420
  77. package/dist/ident/fold.d.ts +24 -15
  78. package/dist/ident/fold.js +12 -145
  79. package/dist/index.d.ts +6 -2
  80. package/dist/index.js +14 -8
  81. package/dist/infer/functions.d.ts +22 -11
  82. package/dist/infer/functions.js +26 -888
  83. package/dist/infer/infer.js +16 -14
  84. package/dist/infer/nullability.js +3 -4
  85. package/dist/infer/types.d.ts +1 -1
  86. package/dist/infer/types.js +7 -7
  87. package/dist/ir/ir.d.ts +26 -17
  88. package/dist/ir/part-span.d.ts +16 -1
  89. package/dist/ir/part-span.js +38 -10
  90. package/dist/ir/span.js +0 -2
  91. package/dist/ir/walk.js +3 -3
  92. package/dist/lineage/hops.js +13 -10
  93. package/dist/lineage/lineage.js +10 -7
  94. package/dist/minijinja/apply-tags.d.ts +14 -7
  95. package/dist/minijinja/apply-tags.js +66 -121
  96. package/dist/minijinja/parse.js +6 -6
  97. package/dist/minijinja/tag-ast.d.ts +29 -36
  98. package/dist/minijinja/tag-ast.js +210 -90
  99. package/dist/mysql/behavior.d.ts +2 -0
  100. package/dist/mysql/behavior.js +21 -0
  101. package/dist/mysql/fold.d.ts +8 -0
  102. package/dist/mysql/fold.js +49 -0
  103. package/dist/mysql/index.d.ts +7 -0
  104. package/dist/mysql/index.js +10 -0
  105. package/dist/mysql/infer.d.ts +20 -0
  106. package/dist/mysql/infer.js +156 -0
  107. package/dist/mysql/lower.d.ts +13 -0
  108. package/dist/mysql/lower.js +1443 -0
  109. package/dist/mysql/parse.d.ts +10 -0
  110. package/dist/mysql/parse.js +70 -0
  111. package/dist/mysql/signatures.generated.d.ts +6 -0
  112. package/dist/mysql/signatures.generated.js +508 -0
  113. package/dist/postgres/behavior.d.ts +2 -0
  114. package/dist/postgres/behavior.js +19 -0
  115. package/dist/postgres/fold.d.ts +8 -0
  116. package/dist/postgres/fold.js +30 -0
  117. package/dist/postgres/index.d.ts +7 -0
  118. package/dist/postgres/index.js +10 -0
  119. package/dist/{infer/postgres.d.ts → postgres/infer.d.ts} +2 -2
  120. package/dist/{infer/postgres.js → postgres/infer.js} +4 -3
  121. package/dist/postgres/lower.js +2 -2
  122. package/dist/postgres/signatures.generated.d.ts +6 -0
  123. package/dist/postgres/signatures.generated.js +2973 -0
  124. package/dist/qualify/check-calls.js +80 -134
  125. package/dist/qualify/qualify.js +16 -14
  126. package/dist/qualify/schema-provider.js +2 -2
  127. package/dist/qualify/schema.js +3 -3
  128. package/dist/qualify/template-provider.d.ts +45 -12
  129. package/dist/qualify/template-provider.js +69 -38
  130. package/dist/redshift/behavior.d.ts +2 -0
  131. package/dist/redshift/behavior.js +19 -0
  132. package/dist/redshift/fold.d.ts +8 -0
  133. package/dist/redshift/fold.js +31 -0
  134. package/dist/redshift/index.d.ts +7 -0
  135. package/dist/redshift/index.js +10 -0
  136. package/dist/{infer/redshift.d.ts → redshift/infer.d.ts} +2 -2
  137. package/dist/{infer/redshift.js → redshift/infer.js} +4 -3
  138. package/dist/redshift/lower.js +2 -2
  139. package/dist/redshift/signatures.generated.d.ts +6 -0
  140. package/dist/redshift/signatures.generated.js +757 -0
  141. package/dist/references/references.js +17 -12
  142. package/dist/scope/like-pattern.d.ts +2 -0
  143. package/dist/scope/like-pattern.js +15 -0
  144. package/dist/scope/scope.d.ts +5 -5
  145. package/dist/scope/scope.js +48 -42
  146. package/dist/sema/resolve.js +17 -12
  147. package/dist/session.d.ts +2 -2
  148. package/dist/signature/signature.d.ts +14 -6
  149. package/dist/signature/signature.js +30 -22
  150. package/dist/signature/signatures.d.ts +14 -12
  151. package/dist/signature/signatures.js +42 -582
  152. package/dist/snowflake/behavior.d.ts +2 -0
  153. package/dist/snowflake/behavior.js +22 -0
  154. package/dist/snowflake/fold.d.ts +8 -0
  155. package/dist/snowflake/fold.js +25 -0
  156. package/dist/snowflake/index.d.ts +7 -0
  157. package/dist/snowflake/index.js +10 -0
  158. package/dist/{infer/snowflake.d.ts → snowflake/infer.d.ts} +2 -2
  159. package/dist/{infer/snowflake.js → snowflake/infer.js} +4 -3
  160. package/dist/snowflake/lower.js +59 -19
  161. package/dist/snowflake/signatures.generated.d.ts +6 -0
  162. package/dist/snowflake/signatures.generated.js +2080 -0
  163. package/dist/sqlite/behavior.d.ts +2 -0
  164. package/dist/sqlite/behavior.js +19 -0
  165. package/dist/sqlite/fold.d.ts +8 -0
  166. package/dist/sqlite/fold.js +41 -0
  167. package/dist/sqlite/index.d.ts +7 -0
  168. package/dist/sqlite/index.js +10 -0
  169. package/dist/sqlite/infer.d.ts +12 -0
  170. package/dist/sqlite/infer.js +122 -0
  171. package/dist/sqlite/lower.d.ts +11 -0
  172. package/dist/sqlite/lower.js +1093 -0
  173. package/dist/sqlite/parse.d.ts +10 -0
  174. package/dist/sqlite/parse.js +70 -0
  175. package/dist/sqlite/signatures.generated.d.ts +6 -0
  176. package/dist/sqlite/signatures.generated.js +277 -0
  177. package/dist/symbols/symbols.js +14 -12
  178. package/dist/token/classify.js +31 -0
  179. package/dist/token/tokenize.js +4 -0
  180. package/dist/trino/behavior.d.ts +2 -0
  181. package/dist/trino/behavior.js +21 -0
  182. package/dist/trino/fold.d.ts +8 -0
  183. package/dist/trino/fold.js +39 -0
  184. package/dist/trino/index.d.ts +7 -0
  185. package/dist/trino/index.js +10 -0
  186. package/dist/{infer/trino.d.ts → trino/infer.d.ts} +2 -2
  187. package/dist/{infer/trino.js → trino/infer.js} +4 -3
  188. package/dist/trino/lower.js +5 -5
  189. package/dist/trino/signatures.generated.d.ts +6 -0
  190. package/dist/trino/signatures.generated.js +968 -0
  191. package/dist/tsql/behavior.d.ts +2 -0
  192. package/dist/tsql/behavior.js +20 -0
  193. package/dist/tsql/fold.d.ts +8 -0
  194. package/dist/tsql/fold.js +34 -0
  195. package/dist/tsql/index.d.ts +7 -0
  196. package/dist/tsql/index.js +10 -0
  197. package/dist/tsql/infer.d.ts +16 -0
  198. package/dist/tsql/infer.js +289 -0
  199. package/dist/tsql/lower.js +4 -4
  200. package/dist/tsql/signatures.generated.d.ts +6 -0
  201. package/dist/tsql/signatures.generated.js +640 -0
  202. package/package.json +15 -11
  203. package/dist/generated/bigquery/GoogleSQLLexer.d.ts +0 -407
  204. package/dist/generated/bigquery/GoogleSQLParser.d.ts +0 -9558
  205. package/dist/generated/bigquery/GoogleSQLParserListener.d.ts +0 -7777
  206. package/dist/generated/bigquery/GoogleSQLParserListener.js +0 -7070
  207. package/dist/generated/databricks/DatabricksLexer.d.ts +0 -566
  208. package/dist/generated/databricks/DatabricksParser.d.ts +0 -7771
  209. package/dist/generated/databricks/DatabricksParserListener.d.ts +0 -5737
  210. package/dist/generated/databricks/DatabricksParserListener.js +0 -5256
  211. package/dist/generated/duckdb/DuckdbLexer.d.ts +0 -691
  212. package/dist/generated/duckdb/DuckdbParser.d.ts +0 -13932
  213. package/dist/generated/duckdb/DuckdbParserListener.d.ts +0 -10049
  214. package/dist/generated/duckdb/DuckdbParserListener.js +0 -9138
  215. package/dist/generated/minijinja/MinijinjaLexer.d.ts +0 -108
  216. package/dist/generated/minijinja/MinijinjaParser.d.ts +0 -604
  217. package/dist/generated/minijinja/MinijinjaParserListener.d.ts +0 -449
  218. package/dist/generated/minijinja/MinijinjaParserListener.js +0 -410
  219. package/dist/generated/postgres/PostgresLexer.d.ts +0 -663
  220. package/dist/generated/postgres/PostgresParser.d.ts +0 -12963
  221. package/dist/generated/postgres/PostgresParserListener.d.ts +0 -9408
  222. package/dist/generated/postgres/PostgresParserListener.js +0 -8554
  223. package/dist/generated/redshift/RedshiftLexer.d.ts +0 -954
  224. package/dist/generated/redshift/RedshiftParser.d.ts +0 -16939
  225. package/dist/generated/redshift/RedshiftParserListener.d.ts +0 -12092
  226. package/dist/generated/redshift/RedshiftParserListener.js +0 -10994
  227. package/dist/generated/snowflake/SnowflakeLexer.d.ts +0 -1046
  228. package/dist/generated/snowflake/SnowflakeParser.d.ts +0 -14196
  229. package/dist/generated/snowflake/SnowflakeParserListener.d.ts +0 -8063
  230. package/dist/generated/snowflake/SnowflakeParserListener.js +0 -7330
  231. package/dist/generated/trino/TrinoLexer.d.ts +0 -381
  232. package/dist/generated/trino/TrinoParser.d.ts +0 -5340
  233. package/dist/generated/trino/TrinoParserListener.d.ts +0 -4704
  234. package/dist/generated/trino/TrinoParserListener.js +0 -4326
  235. package/dist/generated/tsql/TSqlLexer.d.ts +0 -1278
  236. package/dist/generated/tsql/TSqlParser.d.ts +0 -17267
  237. package/dist/generated/tsql/TSqlParserListener.d.ts +0 -9697
  238. package/dist/generated/tsql/TSqlParserListener.js +0 -8854
  239. package/dist/infer/dialect.d.ts +0 -21
  240. package/dist/infer/dialect.js +0 -74
  241. package/dist/infer/literals.d.ts +0 -6
  242. package/dist/infer/literals.js +0 -44
  243. package/dist/signature/generated/tsql.d.ts +0 -3
  244. package/dist/signature/generated/tsql.js +0 -260
@@ -1,4 +1,4 @@
1
- import { type Parser } from "antlr4ng";
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(parser: Parser, startRuleIndex: number, caretTokenIndex: number, preferredRules: Set<number>, ignoredTokens: Set<number>): Candidates;
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(parser, startRuleIndex, caretTokenIndex, preferredRules, ignoredTokens) {
15
- const walk = new CandidateWalk(parser, caretTokenIndex, preferredRules, ignoredTokens);
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
- parser;
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(parser, caretTokenIndex, preferredRules, ignoredTokens) {
36
- this.parser = parser;
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
- const stream = this.parser.inputStream;
43
- for (let i = 0; i <= caretTokenIndex; i++) {
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.parser.atn.ruleToStartState[startRuleIndex];
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.parser.atn.maxTokenType;
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 — e.g. a column's type when the schema knows it. */
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 runs its OWN error-tolerant lex+parse of the current
6
- // text (via makeParser), positions the ATN candidate walk at the caret, and turns
7
- // the raw {tokens, rules} the walk reports into editor completion items:
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 { displayName, foldIdentifier } from "../ident/fold.js";
19
- import { inferDialect } from "../infer/dialect.js";
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 { makeParser } from "./parser-factory.js";
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 ATN walk parses that cell's text (with a
43
- // cell-relative caret) and the visible-column lookup runs over that cell's own scope tree — so a
44
- // caret in statement 2 of a multi-statement document completes through its real scope, not the
45
- // compound facade. Single-cell: the cell IS the document, so this is identical to a whole-doc walk.
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
- // Completion runs its own error-tolerant parse to position the walk (expected — the walk needs
52
- // a parser whose ATN we DFS, not the document's valid-parse CST).
53
- const m = makeParser(cellText, dialect);
54
- // runEntry() first: the CommonTokenStream fills lazily, so the full token list (needed to find
55
- // the caret token) only exists after the parse drives it.
56
- m.runEntry();
57
- const caretIdx = caretTokenIndex(m, cellOffset);
58
- const cand = collectCandidates(m.parser, m.entryRuleIndex, caretIdx, cfg.preferredRules, cfg.ignoredTokens);
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(m, type);
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(m, cfg, schema, dialect))
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(inferDialect(dialect).functions))
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 token's index. Mirrors Task 10's tests' caret helper. */
100
- function caretTokenIndex(m, offset) {
101
- const toks = m.tokenStream.getTokens();
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(m, type) {
115
- const literal = m.lexer.vocabulary.getLiteralName(type);
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 ‹caret› FROM t` as
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(m, cfg, schema, dialect) {
180
+ function fromRelationColumns(walkTokens, cfg, schema, dialect, tags, text) {
138
181
  if (cfg.relationKeywordTokens.size === 0)
139
182
  return [];
140
- // Default-channel tokens only — hidden whitespace/comments sit between FROM and the name.
141
- const toks = m.tokenStream.getTokens().filter((t) => t.channel === Token.DEFAULT_CHANNEL);
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 t = toks[i];
145
- const n = toks[i + 1];
146
- if (!t || !n)
192
+ const kw = toks[i];
193
+ const next = toks[i + 1];
194
+ if (!kw || !next)
147
195
  continue;
148
- if (!cfg.relationKeywordTokens.has(t.type))
196
+ if (!cfg.relationKeywordTokens.has(kw.type))
149
197
  continue;
150
- if (!cfg.nameTokens.has(n.type))
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
- const cols = schema.columnsFor([n.text ?? ""], dialect);
153
- if (!cols)
154
- continue;
155
- for (const c of cols)
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 = foldIdentifier(col.label, dialect);
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, dialect) });
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;