@tablecraft-app/plugin-api 1.3.0 → 1.4.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/dist/completion.d.ts +6 -1
- package/dist/sql/complete.js +67 -8
- package/dist/sql/functions.d.ts +20 -4
- package/dist/sql/functions.js +115 -24
- package/dist/sql/signature.d.ts +9 -1
- package/dist/sql/signature.js +48 -10
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/completion.d.ts
CHANGED
|
@@ -22,6 +22,8 @@ export interface FunctionParameter {
|
|
|
22
22
|
readonly optional?: boolean;
|
|
23
23
|
/** Repeats: the call may pass it any number of times. */
|
|
24
24
|
readonly variadic?: boolean;
|
|
25
|
+
/** The keyword written before it instead of a comma, such as `FROM` in `EXTRACT(unit FROM date)`; `|` separates alternatives. */
|
|
26
|
+
readonly lead?: string;
|
|
25
27
|
}
|
|
26
28
|
/** One way to call a function: its parameters in order and what it returns. */
|
|
27
29
|
export interface FunctionSignature {
|
|
@@ -33,7 +35,10 @@ export interface SignatureHelp {
|
|
|
33
35
|
readonly name: string;
|
|
34
36
|
readonly summary: string;
|
|
35
37
|
readonly signatures: readonly FunctionSignature[];
|
|
36
|
-
/**
|
|
38
|
+
/**
|
|
39
|
+
* The zero-based parameter the caret is in: the top-level commas before it inside the call, or,
|
|
40
|
+
* when the signature has keyword-led parameters (`lead`), the one the separators walk to.
|
|
41
|
+
*/
|
|
37
42
|
readonly argument: number;
|
|
38
43
|
/** The call's name to the end of its arguments, or to the caret while it is still open. */
|
|
39
44
|
readonly from: number;
|
package/dist/sql/complete.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { functionItem, functionName, isAvailable } from "./functions.js";
|
|
1
|
+
import { functionItem, functionName, functionPackage, isAvailable, leadMatches, memberItem, } from "./functions.js";
|
|
2
2
|
import { callAt } from "./signature.js";
|
|
3
3
|
import { parseSimpleSelect, SELECT_READ_ONLY_REASONS } from "./select.js";
|
|
4
4
|
import { scopeAt } from "./scope.js";
|
|
@@ -76,6 +76,50 @@ const QUOTES = new Set(['"', "`", "["]);
|
|
|
76
76
|
/** Below every other namespace: `USE mysql` is legitimate but rarely meant. */
|
|
77
77
|
const SYSTEM_NAMESPACE_BOOST = -20;
|
|
78
78
|
const CURRENT_DETAIL = "current";
|
|
79
|
+
const COMMA = ",";
|
|
80
|
+
const MEMBER_SEPARATOR = ".";
|
|
81
|
+
/**
|
|
82
|
+
* Walks `separators` through `signature`: a comma moves to the next parameter without a lead, a
|
|
83
|
+
* word to the next one it leads. A word nothing leads is an argument's own (`YEAR`, `DECIMAL`).
|
|
84
|
+
* Past the last parameter a comma keeps counting, as the hint's variadic rule expects; a variadic
|
|
85
|
+
* parameter keyword-led ones follow (`CHAR(code... USING charset)`) takes every comma.
|
|
86
|
+
*/
|
|
87
|
+
function argumentAt(signature, separators) {
|
|
88
|
+
const { parameters } = signature;
|
|
89
|
+
let at = 0;
|
|
90
|
+
let entered = false;
|
|
91
|
+
let led = 0;
|
|
92
|
+
for (const separator of separators) {
|
|
93
|
+
if (separator === COMMA) {
|
|
94
|
+
const next = parameters.findIndex((parameter, index) => index > at && parameter.lead === undefined);
|
|
95
|
+
if (next >= 0)
|
|
96
|
+
at = next;
|
|
97
|
+
else if (!(parameters[at]?.variadic === true && at < parameters.length - 1))
|
|
98
|
+
at += 1;
|
|
99
|
+
entered = true;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
const next = parameters.findIndex((parameter, index) => (index > at || (index === at && !entered)) &&
|
|
103
|
+
parameter.lead !== undefined &&
|
|
104
|
+
leadMatches(parameter.lead, separator));
|
|
105
|
+
if (next < 0)
|
|
106
|
+
continue;
|
|
107
|
+
at = next;
|
|
108
|
+
entered = true;
|
|
109
|
+
led += 1;
|
|
110
|
+
}
|
|
111
|
+
return { argument: at, led };
|
|
112
|
+
}
|
|
113
|
+
/** The argument the caret is in, by the overload whose leads the call follows most; the first on a tie. */
|
|
114
|
+
function argumentOf(spec, call) {
|
|
115
|
+
let best = null;
|
|
116
|
+
for (const signature of spec.signatures) {
|
|
117
|
+
const found = argumentAt(signature, call.separators ?? []);
|
|
118
|
+
if (best === null || found.led > best.led)
|
|
119
|
+
best = found;
|
|
120
|
+
}
|
|
121
|
+
return best?.argument ?? call.argument;
|
|
122
|
+
}
|
|
79
123
|
function relationKind(relation) {
|
|
80
124
|
return relation.kind === "table" ? "table" : "view";
|
|
81
125
|
}
|
|
@@ -93,6 +137,13 @@ export function sqlCompleter(options) {
|
|
|
93
137
|
function specNamed(name, server) {
|
|
94
138
|
return specs.get(name.toUpperCase())?.find((spec) => isAvailable(spec, server));
|
|
95
139
|
}
|
|
140
|
+
/** The members of the package `qualifier` names, as offered after its dot. */
|
|
141
|
+
function memberItems(qualifier, server) {
|
|
142
|
+
const name = qualifier.map((part) => part.name.toUpperCase()).join(MEMBER_SEPARATOR);
|
|
143
|
+
return availableFunctions(server, true)
|
|
144
|
+
.filter((spec) => typeof spec !== "string" && functionPackage(spec) === name)
|
|
145
|
+
.map((spec) => memberItem(spec, BOOST.function));
|
|
146
|
+
}
|
|
96
147
|
function quoted(name) {
|
|
97
148
|
return options.plainIdentifier.test(name) && !reserved.has(name.toUpperCase())
|
|
98
149
|
? name
|
|
@@ -115,10 +166,13 @@ export function sqlCompleter(options) {
|
|
|
115
166
|
.filter((keyword) => !except.has(keyword.toUpperCase()))
|
|
116
167
|
.map((keyword) => item("keyword", keyword, keyword, null));
|
|
117
168
|
}
|
|
118
|
-
|
|
169
|
+
/** Plain functions, or with `members` the package members only: a bare name never offers `pkg.member`. */
|
|
170
|
+
function availableFunctions(server, members = false) {
|
|
119
171
|
const offered = new Set();
|
|
120
172
|
return options.functions.filter((spec) => {
|
|
121
173
|
const name = functionName(spec).toUpperCase();
|
|
174
|
+
if ((functionPackage(spec) !== null) !== members)
|
|
175
|
+
return false;
|
|
122
176
|
if (offered.has(name) || (typeof spec !== "string" && !isAvailable(spec, server)))
|
|
123
177
|
return false;
|
|
124
178
|
offered.add(name);
|
|
@@ -131,21 +185,22 @@ export function sqlCompleter(options) {
|
|
|
131
185
|
if (last === undefined)
|
|
132
186
|
return [];
|
|
133
187
|
const relational = clause === "relation" || clause === "join";
|
|
188
|
+
const members = relational ? [] : memberItems(qualifier, lookup.server);
|
|
134
189
|
if (qualifier.length === 1 && !relational) {
|
|
135
190
|
const source = sourceNamed(query, last);
|
|
136
191
|
if (source !== undefined)
|
|
137
|
-
return columnItems(await lookup.columnsOf(source, query));
|
|
192
|
+
return [...columnItems(await lookup.columnsOf(source, query)), ...members];
|
|
138
193
|
const cte = cteNamed(query, last);
|
|
139
194
|
if (cte !== undefined)
|
|
140
|
-
return columnItems(await lookup.cteColumns(cte, 1));
|
|
195
|
+
return [...columnItems(await lookup.cteColumns(cte, 1)), ...members];
|
|
141
196
|
}
|
|
142
197
|
const namespace = await lookup.namespace(qualifier);
|
|
143
198
|
if (namespace !== undefined)
|
|
144
|
-
return relationItems(await lookup.relations(namespace.path), null);
|
|
199
|
+
return [...members, ...relationItems(await lookup.relations(namespace.path), null)];
|
|
145
200
|
if (relational)
|
|
146
201
|
return [];
|
|
147
202
|
const found = await lookup.relation(qualifier.slice(0, -1), last);
|
|
148
|
-
return found === null ?
|
|
203
|
+
return found === null ? members : [...columnItems(found.relation.columns), ...members];
|
|
149
204
|
}
|
|
150
205
|
async function relationContextItems(scope, lookup) {
|
|
151
206
|
const current = lookup.current;
|
|
@@ -463,7 +518,11 @@ export function sqlCompleter(options) {
|
|
|
463
518
|
function signatureAt(request, catalog) {
|
|
464
519
|
const { text, offset, shift } = wrap(request);
|
|
465
520
|
const server = serverOf(catalog);
|
|
466
|
-
const
|
|
521
|
+
const callable = (name) => {
|
|
522
|
+
const spec = specNamed(name, server);
|
|
523
|
+
return spec !== undefined && spec.bare !== true;
|
|
524
|
+
};
|
|
525
|
+
const call = callAt(text, offset, options.dialect, reserved, callable);
|
|
467
526
|
const spec = call === null ? undefined : specNamed(call.name, server);
|
|
468
527
|
if (call === null || spec === undefined)
|
|
469
528
|
return Promise.resolve(null);
|
|
@@ -471,7 +530,7 @@ export function sqlCompleter(options) {
|
|
|
471
530
|
name: spec.name.toLowerCase(),
|
|
472
531
|
summary: spec.summary,
|
|
473
532
|
signatures: spec.signatures,
|
|
474
|
-
argument: call
|
|
533
|
+
argument: argumentOf(spec, call),
|
|
475
534
|
from: call.from - shift,
|
|
476
535
|
to: call.to - shift,
|
|
477
536
|
});
|
package/dist/sql/functions.d.ts
CHANGED
|
@@ -21,26 +21,42 @@ export interface FunctionSpec {
|
|
|
21
21
|
readonly summary: string;
|
|
22
22
|
readonly category: FunctionCategory;
|
|
23
23
|
readonly available?: ServerRange;
|
|
24
|
+
/** Written without parentheses, such as Oracle's `SYSDATE`; the call is the name alone and has no hint. */
|
|
25
|
+
readonly bare?: true;
|
|
24
26
|
}
|
|
25
|
-
/**
|
|
27
|
+
/**
|
|
28
|
+
* `[name, signatures, summary, available?]`, a signature written `"date, format -> string"`, a
|
|
29
|
+
* keyword-led parameter `"unit FROM date -> integer"`, a bare function `"= date"` and a procedure
|
|
30
|
+
* `"str ->"`. A name `PKG.MEMBER` is offered after `pkg.`.
|
|
31
|
+
*/
|
|
26
32
|
export type FunctionEntry = readonly [
|
|
27
33
|
name: string,
|
|
28
34
|
signatures: string | readonly string[],
|
|
29
35
|
summary: string,
|
|
30
36
|
available?: ServerRange
|
|
31
37
|
];
|
|
32
|
-
/**
|
|
38
|
+
/** Whether `lead` is `word` or offers it among its `|` alternatives. */
|
|
39
|
+
export declare function leadMatches(lead: string, word: string): boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Reads `"str, len? -> string"`: `?` marks an optional parameter, `...` one that repeats, an
|
|
42
|
+
* upper-case word between names the keyword before the next (`"unit FROM date -> integer"`).
|
|
43
|
+
* `"= date"` is a bare function's and `"str ->"` a procedure's, which returns nothing.
|
|
44
|
+
*/
|
|
33
45
|
export declare function parseSignature(written: string): FunctionSignature;
|
|
34
46
|
/** Specs for one category, from compact entries a plugin's data file lists. */
|
|
35
47
|
export declare function functionSpecs(category: FunctionCategory, entries: readonly FunctionEntry[]): FunctionSpec[];
|
|
36
48
|
/** Whether `spec` exists on `server`; an unknown server has every function any flavour has. */
|
|
37
49
|
export declare function isAvailable(spec: FunctionSpec, server: ServerIdentity | null): boolean;
|
|
38
|
-
/** `(date, format) → string`, the popup's detail for one signature
|
|
50
|
+
/** `(date, format) → string`, the popup's detail for one signature; a lead replaces the comma, a procedure has no `→`. */
|
|
39
51
|
export declare function signatureLabel(signature: FunctionSignature): string;
|
|
40
52
|
/** Every signature on its own line, then the summary. */
|
|
41
53
|
export declare function functionInfo(spec: FunctionSpec): string;
|
|
42
|
-
/** A spec as an item: `name()` with the caret between the parentheses when it takes arguments. */
|
|
54
|
+
/** A spec as an item: `name()` with the caret between the parentheses when it takes arguments, the name alone when bare. */
|
|
43
55
|
export declare function functionItem(spec: FunctionSpec | string, boost: number): CompletionItem;
|
|
56
|
+
/** The package a spec named `PKG.MEMBER` belongs to, upper case; null for a plain function. */
|
|
57
|
+
export declare function functionPackage(spec: FunctionSpec | string): string | null;
|
|
58
|
+
/** A package member as offered after `pkg.`: the member's name alone, the info naming it in full. */
|
|
59
|
+
export declare function memberItem(spec: FunctionSpec, boost: number): CompletionItem;
|
|
44
60
|
/** The name a function entry is offered under, either form. */
|
|
45
61
|
export declare function functionName(spec: FunctionSpec | string): string;
|
|
46
62
|
/** Aggregate and scalar functions every built-in dialect has; a plugin adds its own. Written in our own words. */
|
package/dist/sql/functions.js
CHANGED
|
@@ -20,7 +20,13 @@ const SHOWN_ARROW = "→";
|
|
|
20
20
|
const OPTIONAL_MARK = "?";
|
|
21
21
|
const VARIADIC_MARK = "...";
|
|
22
22
|
const COMPARATOR = /^(>=|<=|>|<|=)?(\d+(?:\.\d+)*)$/;
|
|
23
|
-
|
|
23
|
+
const BARE_MARK = "=";
|
|
24
|
+
const LEAD_WORD = /^[A-Z][A-Z_]*$/;
|
|
25
|
+
const LEAD_CHOICE = "|";
|
|
26
|
+
const PARAMETER_SEPARATOR = ",";
|
|
27
|
+
const COMMA_SHOWN = ", ";
|
|
28
|
+
const MEMBER_SEPARATOR = ".";
|
|
29
|
+
function parameter(written, lead) {
|
|
24
30
|
let name = written.trim();
|
|
25
31
|
const variadic = name.endsWith(VARIADIC_MARK);
|
|
26
32
|
if (variadic)
|
|
@@ -28,28 +34,85 @@ function parameter(written) {
|
|
|
28
34
|
const optional = name.endsWith(OPTIONAL_MARK);
|
|
29
35
|
if (optional)
|
|
30
36
|
name = name.slice(0, -OPTIONAL_MARK.length).trim();
|
|
31
|
-
return {
|
|
37
|
+
return {
|
|
38
|
+
name,
|
|
39
|
+
...(optional ? { optional } : {}),
|
|
40
|
+
...(variadic ? { variadic } : {}),
|
|
41
|
+
...(lead === null ? {} : { lead }),
|
|
42
|
+
};
|
|
32
43
|
}
|
|
33
|
-
|
|
44
|
+
function isLeadWord(word) {
|
|
45
|
+
return word.split(LEAD_CHOICE).every((choice) => LEAD_WORD.test(choice));
|
|
46
|
+
}
|
|
47
|
+
/** Whether `lead` is `word` or offers it among its `|` alternatives. */
|
|
48
|
+
export function leadMatches(lead, word) {
|
|
49
|
+
return lead.split(LEAD_CHOICE).includes(word);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* One comma-separated part: an upper-case word between names leads the next, as one before the
|
|
53
|
+
* first parameter does; one standing alone is a keyword choice. After a comma the keyword is the
|
|
54
|
+
* argument's own (`date, INTERVAL expr unit`), since the comma already separates it.
|
|
55
|
+
*/
|
|
56
|
+
function leadParameters(part, index) {
|
|
57
|
+
const words = part.trim().split(/\s+/);
|
|
58
|
+
const parameters = [];
|
|
59
|
+
let lead = null;
|
|
60
|
+
let name = [];
|
|
61
|
+
const flush = () => {
|
|
62
|
+
if (name.length > 0)
|
|
63
|
+
parameters.push(parameter(name.join(" "), lead));
|
|
64
|
+
name = [];
|
|
65
|
+
lead = null;
|
|
66
|
+
};
|
|
67
|
+
words.forEach((word, at) => {
|
|
68
|
+
const next = words[at + 1];
|
|
69
|
+
const afterComma = at === 0 && index > 0;
|
|
70
|
+
if (!afterComma && isLeadWord(word) && next !== undefined && !isLeadWord(next)) {
|
|
71
|
+
flush();
|
|
72
|
+
lead = word;
|
|
73
|
+
}
|
|
74
|
+
else
|
|
75
|
+
name.push(word);
|
|
76
|
+
});
|
|
77
|
+
flush();
|
|
78
|
+
return parameters;
|
|
79
|
+
}
|
|
80
|
+
function isBareSignature(written) {
|
|
81
|
+
return written.trim().startsWith(BARE_MARK);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Reads `"str, len? -> string"`: `?` marks an optional parameter, `...` one that repeats, an
|
|
85
|
+
* upper-case word between names the keyword before the next (`"unit FROM date -> integer"`).
|
|
86
|
+
* `"= date"` is a bare function's and `"str ->"` a procedure's, which returns nothing.
|
|
87
|
+
*/
|
|
34
88
|
export function parseSignature(written) {
|
|
89
|
+
if (isBareSignature(written))
|
|
90
|
+
return { parameters: [], returns: written.trim().slice(BARE_MARK.length).trim() };
|
|
35
91
|
const arrow = written.lastIndexOf(RETURNS_ARROW);
|
|
36
92
|
if (arrow < 0)
|
|
37
93
|
throw new Error(`Signature "${written}" has no "${RETURNS_ARROW} type"; add what it returns.`);
|
|
38
94
|
const list = written.slice(0, arrow).trim();
|
|
39
95
|
return {
|
|
40
|
-
parameters: list === "" ? [] : list.split(
|
|
96
|
+
parameters: list === "" ? [] : list.split(PARAMETER_SEPARATOR).flatMap(leadParameters),
|
|
41
97
|
returns: written.slice(arrow + RETURNS_ARROW.length).trim(),
|
|
42
98
|
};
|
|
43
99
|
}
|
|
44
100
|
/** Specs for one category, from compact entries a plugin's data file lists. */
|
|
45
101
|
export function functionSpecs(category, entries) {
|
|
46
|
-
return entries.map(([name, signatures, summary, available]) =>
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
102
|
+
return entries.map(([name, signatures, summary, available]) => {
|
|
103
|
+
const written = typeof signatures === "string" ? [signatures] : signatures;
|
|
104
|
+
const bare = written.filter(isBareSignature).length;
|
|
105
|
+
if (bare > 0 && bare < written.length)
|
|
106
|
+
throw new Error(`${name} mixes "${BARE_MARK} type" with called signatures; write the bare form as "-> type".`);
|
|
107
|
+
return {
|
|
108
|
+
name,
|
|
109
|
+
signatures: written.map(parseSignature),
|
|
110
|
+
summary,
|
|
111
|
+
category,
|
|
112
|
+
...(available === undefined ? {} : { available }),
|
|
113
|
+
...(bare > 0 ? { bare: true } : {}),
|
|
114
|
+
};
|
|
115
|
+
});
|
|
53
116
|
}
|
|
54
117
|
function compareVersions(left, right) {
|
|
55
118
|
for (let at = 0; at < Math.max(left.length, right.length); at += 1) {
|
|
@@ -91,37 +154,65 @@ export function isAvailable(spec, server) {
|
|
|
91
154
|
.filter((comparator) => comparator !== "")
|
|
92
155
|
.every((comparator) => satisfies(server.version, comparator));
|
|
93
156
|
}
|
|
94
|
-
function shownParameter(parameter) {
|
|
157
|
+
function shownParameter(parameter, index) {
|
|
95
158
|
const name = parameter.variadic === true ? `${parameter.name}${VARIADIC_MARK}` : parameter.name;
|
|
96
|
-
|
|
159
|
+
const led = parameter.lead === undefined ? name : `${parameter.lead} ${name}`;
|
|
160
|
+
const separator = index === 0 ? "" : parameter.lead === undefined ? COMMA_SHOWN : " ";
|
|
161
|
+
return separator + (parameter.optional === true ? `[${led}]` : led);
|
|
97
162
|
}
|
|
98
|
-
|
|
163
|
+
function returnsLabel(returns) {
|
|
164
|
+
return returns === "" ? "" : `${SHOWN_ARROW} ${returns}`;
|
|
165
|
+
}
|
|
166
|
+
/** `(date, format) → string`, the popup's detail for one signature; a lead replaces the comma, a procedure has no `→`. */
|
|
99
167
|
export function signatureLabel(signature) {
|
|
100
|
-
|
|
168
|
+
const list = `(${signature.parameters.map(shownParameter).join("")})`;
|
|
169
|
+
const returns = returnsLabel(signature.returns);
|
|
170
|
+
return returns === "" ? list : `${list} ${returns}`;
|
|
171
|
+
}
|
|
172
|
+
function shownSignature(spec, signature) {
|
|
173
|
+
return spec.bare === true ? returnsLabel(signature.returns) : signatureLabel(signature);
|
|
101
174
|
}
|
|
102
175
|
/** Every signature on its own line, then the summary. */
|
|
103
176
|
export function functionInfo(spec) {
|
|
104
177
|
const label = spec.name.toLowerCase();
|
|
105
|
-
const
|
|
178
|
+
const gap = spec.bare === true ? " " : "";
|
|
179
|
+
const lines = spec.signatures.map((signature) => `${label}${gap}${shownSignature(spec, signature)}`);
|
|
106
180
|
return `${lines.join("\n")}\n\n${spec.summary}`;
|
|
107
181
|
}
|
|
108
|
-
/**
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
182
|
+
/**
|
|
183
|
+
* A spec as an item: `name()` with the caret between the parentheses when it takes arguments, or
|
|
184
|
+
* the name alone for a bare one. `label` is what the popup offers, the member alone after `pkg.`.
|
|
185
|
+
*/
|
|
186
|
+
function specItem(spec, label, boost) {
|
|
113
187
|
const first = spec.signatures[0];
|
|
114
188
|
const takesArguments = spec.signatures.some((signature) => signature.parameters.length > 0);
|
|
189
|
+
const bare = spec.bare === true;
|
|
115
190
|
return {
|
|
116
191
|
label,
|
|
117
192
|
kind: "function",
|
|
118
|
-
detail: first === undefined ? null :
|
|
119
|
-
apply: `${label}()`,
|
|
193
|
+
detail: first === undefined ? null : shownSignature(spec, first),
|
|
194
|
+
apply: bare ? label : `${label}()`,
|
|
120
195
|
boost,
|
|
121
196
|
info: functionInfo(spec),
|
|
122
|
-
caret: takesArguments ? label.length + 1 : null,
|
|
197
|
+
caret: takesArguments && !bare ? label.length + 1 : null,
|
|
123
198
|
};
|
|
124
199
|
}
|
|
200
|
+
/** A spec as an item: `name()` with the caret between the parentheses when it takes arguments, the name alone when bare. */
|
|
201
|
+
export function functionItem(spec, boost) {
|
|
202
|
+
if (typeof spec === "string")
|
|
203
|
+
return { label: spec, kind: "function", detail: null, apply: spec, boost, info: null, caret: null };
|
|
204
|
+
return specItem(spec, spec.name.toLowerCase(), boost);
|
|
205
|
+
}
|
|
206
|
+
/** The package a spec named `PKG.MEMBER` belongs to, upper case; null for a plain function. */
|
|
207
|
+
export function functionPackage(spec) {
|
|
208
|
+
const name = functionName(spec);
|
|
209
|
+
const dot = name.lastIndexOf(MEMBER_SEPARATOR);
|
|
210
|
+
return dot < 0 ? null : name.slice(0, dot).toUpperCase();
|
|
211
|
+
}
|
|
212
|
+
/** A package member as offered after `pkg.`: the member's name alone, the info naming it in full. */
|
|
213
|
+
export function memberItem(spec, boost) {
|
|
214
|
+
return specItem(spec, spec.name.slice(spec.name.lastIndexOf(MEMBER_SEPARATOR) + 1).toLowerCase(), boost);
|
|
215
|
+
}
|
|
125
216
|
/** The name a function entry is offered under, either form. */
|
|
126
217
|
export function functionName(spec) {
|
|
127
218
|
return typeof spec === "string" ? spec : spec.name;
|
package/dist/sql/signature.d.ts
CHANGED
|
@@ -3,13 +3,21 @@ import type { SqlDialectOptions } from "./tokens.js";
|
|
|
3
3
|
export interface CallAt {
|
|
4
4
|
/** As written. */
|
|
5
5
|
readonly name: string;
|
|
6
|
+
/** The top-level commas before the caret. */
|
|
6
7
|
readonly argument: number;
|
|
8
|
+
/**
|
|
9
|
+
* The call's top-level `,` and words before the caret, in order, words upper case: what moves a
|
|
10
|
+
* keyword-led parameter list such as `EXTRACT(YEAR FROM d)` along. Always set by `callAt`;
|
|
11
|
+
* optional so a value built against the older shape still type-checks (decision 106).
|
|
12
|
+
*/
|
|
13
|
+
readonly separators?: readonly string[];
|
|
7
14
|
readonly from: number;
|
|
8
15
|
readonly to: number;
|
|
9
16
|
}
|
|
10
17
|
/**
|
|
11
18
|
* Finds the innermost call around `offset` whose name `known` accepts, counting its top-level
|
|
12
|
-
* commas; strings, comments and quoted names are tokens of their own,
|
|
19
|
+
* commas and listing its separators; strings, comments and quoted names are tokens of their own,
|
|
20
|
+
* so their commas never count. `pkg.member (` asks `known` for `PKG.MEMBER`.
|
|
13
21
|
* Grouping parentheses are looked through; a subquery, a non-function name before `(` or a caret
|
|
14
22
|
* in a string or comment ends the search with null. `reserved` holds the dialect's keywords: a
|
|
15
23
|
* function spelled like one (`VALUES`, `LEFT`) is a call only where an operand fits, not after a
|
package/dist/sql/signature.js
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { statementTokens } from "./scope.js";
|
|
2
|
+
const COMMA = ",";
|
|
3
|
+
const MEMBER_SEPARATOR = ".";
|
|
4
|
+
/** Calls a procedure by name, so it may come before a package member as it never may before a function. */
|
|
5
|
+
const CALL_WORD = "CALL";
|
|
2
6
|
/** Words a relation or routine name follows, so `TABLE upper (` defines rather than calls. */
|
|
3
7
|
const NAME_INTRODUCERS = new Set([
|
|
4
8
|
"CALL",
|
|
@@ -25,7 +29,8 @@ function upperWord(text, token) {
|
|
|
25
29
|
}
|
|
26
30
|
/**
|
|
27
31
|
* Finds the innermost call around `offset` whose name `known` accepts, counting its top-level
|
|
28
|
-
* commas; strings, comments and quoted names are tokens of their own,
|
|
32
|
+
* commas and listing its separators; strings, comments and quoted names are tokens of their own,
|
|
33
|
+
* so their commas never count. `pkg.member (` asks `known` for `PKG.MEMBER`.
|
|
29
34
|
* Grouping parentheses are looked through; a subquery, a non-function name before `(` or a caret
|
|
30
35
|
* in a string or comment ends the search with null. `reserved` holds the dialect's keywords: a
|
|
31
36
|
* function spelled like one (`VALUES`, `LEFT`) is a call only where an operand fits, not after a
|
|
@@ -35,23 +40,42 @@ export function callAt(text, offset, dialect, reserved, known) {
|
|
|
35
40
|
const { tokens, literal } = statementTokens(text, offset, dialect);
|
|
36
41
|
if (literal)
|
|
37
42
|
return null;
|
|
38
|
-
|
|
43
|
+
/** `pkg.member` ending at `at`, when the package's spec knows the member. */
|
|
44
|
+
function member(at) {
|
|
39
45
|
const name = tokens[at];
|
|
46
|
+
const owner = tokens[at - 2];
|
|
40
47
|
const word = upperWord(text, name);
|
|
41
|
-
|
|
48
|
+
const ownerWord = upperWord(text, owner);
|
|
49
|
+
if (name === undefined || owner === undefined || word === null || ownerWord === null)
|
|
50
|
+
return null;
|
|
51
|
+
if (!known(`${ownerWord}${MEMBER_SEPARATOR}${word}`))
|
|
42
52
|
return null;
|
|
53
|
+
const previous = upperWord(text, tokens[at - 3]);
|
|
54
|
+
if (previous !== null && previous !== CALL_WORD && NAME_INTRODUCERS.has(previous))
|
|
55
|
+
return null;
|
|
56
|
+
return {
|
|
57
|
+
written: `${text.slice(owner.from, owner.to)}${MEMBER_SEPARATOR}${text.slice(name.from, name.to)}`,
|
|
58
|
+
from: owner.from,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function callable(at) {
|
|
62
|
+
const name = tokens[at];
|
|
43
63
|
const before = tokens[at - 1];
|
|
44
|
-
if (symbol(text, before,
|
|
64
|
+
if (symbol(text, before, MEMBER_SEPARATOR))
|
|
65
|
+
return member(at);
|
|
66
|
+
const word = upperWord(text, name);
|
|
67
|
+
if (name === undefined || word === null || !known(word))
|
|
45
68
|
return null;
|
|
69
|
+
const found = { written: text.slice(name.from, name.to), from: name.from };
|
|
46
70
|
const previous = upperWord(text, before);
|
|
47
71
|
if (previous !== null && NAME_INTRODUCERS.has(previous))
|
|
48
72
|
return null;
|
|
49
73
|
if (!reserved.has(word))
|
|
50
|
-
return
|
|
74
|
+
return found;
|
|
51
75
|
const operandFits = before === undefined ||
|
|
52
76
|
(before.kind === "symbol" && !symbol(text, before, ")")) ||
|
|
53
77
|
(previous !== null && reserved.has(previous));
|
|
54
|
-
return operandFits ?
|
|
78
|
+
return operandFits ? found : null;
|
|
55
79
|
}
|
|
56
80
|
const frames = [];
|
|
57
81
|
let index = 0;
|
|
@@ -61,14 +85,27 @@ export function callAt(text, offset, dialect, reserved, known) {
|
|
|
61
85
|
break;
|
|
62
86
|
if (symbol(text, token, "(")) {
|
|
63
87
|
const name = callable(index - 1);
|
|
64
|
-
frames.push({
|
|
88
|
+
frames.push({
|
|
89
|
+
name,
|
|
90
|
+
named: name === null && !grouping(text, tokens[index - 1]),
|
|
91
|
+
open: index,
|
|
92
|
+
commas: 0,
|
|
93
|
+
separators: [],
|
|
94
|
+
});
|
|
65
95
|
}
|
|
66
96
|
else if (symbol(text, token, ")"))
|
|
67
97
|
frames.pop();
|
|
68
|
-
else if (symbol(text, token,
|
|
98
|
+
else if (symbol(text, token, COMMA)) {
|
|
69
99
|
const top = frames.at(-1);
|
|
70
|
-
if (top !== undefined)
|
|
100
|
+
if (top !== undefined) {
|
|
71
101
|
top.commas += 1;
|
|
102
|
+
top.separators.push(COMMA);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
const word = upperWord(text, token);
|
|
107
|
+
if (word !== null)
|
|
108
|
+
frames.at(-1)?.separators.push(word);
|
|
72
109
|
}
|
|
73
110
|
}
|
|
74
111
|
for (let depth = frames.length - 1; depth >= 0; depth -= 1) {
|
|
@@ -81,8 +118,9 @@ export function callAt(text, offset, dialect, reserved, known) {
|
|
|
81
118
|
continue;
|
|
82
119
|
}
|
|
83
120
|
return {
|
|
84
|
-
name:
|
|
121
|
+
name: frame.name.written,
|
|
85
122
|
argument: frame.commas,
|
|
123
|
+
separators: frame.separators,
|
|
86
124
|
from: frame.name.from,
|
|
87
125
|
to: closeOf(text, tokens, index, frames.length - depth) ?? offset,
|
|
88
126
|
};
|
package/dist/version.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* The plugin contract's version, by semver (decision 106): a host loads a plugin built for the same
|
|
3
3
|
* major and an older or equal minor. Typed `string` so a bump does not change the API report.
|
|
4
4
|
*/
|
|
5
|
-
export const API_VERSION = "1.
|
|
5
|
+
export const API_VERSION = "1.4.0";
|
|
6
6
|
const SEMVER = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
|
|
7
7
|
/** `major.minor.patch` and nothing else; pre-release and build tags are not accepted. */
|
|
8
8
|
export function parseVersion(text) {
|