@telorun/ide-support 0.20.2 → 0.21.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/cel/cursor-chain.d.ts +4 -0
- package/dist/cel/cursor-chain.d.ts.map +1 -1
- package/dist/cel/cursor-chain.js +1 -1
- package/dist/cel/module-calls.d.ts +56 -0
- package/dist/cel/module-calls.d.ts.map +1 -0
- package/dist/cel/module-calls.js +98 -0
- package/dist/cel/tokens.d.ts.map +1 -1
- package/dist/cel/tokens.js +9 -1
- package/dist/completions/cel-completions.d.ts.map +1 -1
- package/dist/completions/cel-completions.js +15 -0
- package/dist/completions/valid-capabilities.d.ts +1 -1
- package/dist/completions/valid-capabilities.d.ts.map +1 -1
- package/dist/completions/valid-capabilities.js +4 -0
- package/dist/definition/build-definition.d.ts.map +1 -1
- package/dist/definition/build-definition.js +46 -2
- package/dist/diagnostics/normalize.d.ts.map +1 -1
- package/dist/diagnostics/normalize.js +9 -1
- package/dist/hover/build-hover.d.ts.map +1 -1
- package/dist/hover/build-hover.js +17 -3
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/rename/build-rename.d.ts +5 -2
- package/dist/rename/build-rename.d.ts.map +1 -1
- package/dist/rename/build-rename.js +41 -14
- package/dist/rename/find-sites.d.ts +5 -0
- package/dist/rename/find-sites.d.ts.map +1 -1
- package/dist/rename/find-sites.js +28 -0
- package/dist/signature-help/build-signature-help.d.ts +15 -0
- package/dist/signature-help/build-signature-help.d.ts.map +1 -0
- package/dist/signature-help/build-signature-help.js +87 -0
- package/dist/signature-help/index.d.ts +2 -0
- package/dist/signature-help/index.d.ts.map +1 -0
- package/dist/signature-help/index.js +1 -0
- package/dist/types.d.ts +19 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/cel/cursor-chain.ts +1 -1
- package/src/cel/module-calls.ts +138 -0
- package/src/cel/tokens.ts +8 -1
- package/src/completions/cel-completions.ts +15 -0
- package/src/completions/valid-capabilities.ts +6 -0
- package/src/definition/build-definition.ts +49 -1
- package/src/diagnostics/normalize.ts +9 -1
- package/src/hover/build-hover.ts +16 -2
- package/src/index.ts +4 -0
- package/src/rename/build-rename.ts +55 -10
- package/src/rename/find-sites.ts +30 -0
- package/src/signature-help/build-signature-help.ts +100 -0
- package/src/signature-help/index.ts +1 -0
- package/src/types.ts +17 -2
|
@@ -9,14 +9,18 @@ import {
|
|
|
9
9
|
type LoadedFile,
|
|
10
10
|
type LoadedGraph,
|
|
11
11
|
type LoadedModule,
|
|
12
|
+
type ManifestAnalysis,
|
|
13
|
+
type NameLevel,
|
|
12
14
|
type Range,
|
|
13
15
|
} from "@telorun/analyzer";
|
|
14
16
|
|
|
15
17
|
import { chainAt } from "../cel-chain.js";
|
|
16
18
|
import { resolveNodeAtPosition, scalarString } from "../completions/resolve-node.js";
|
|
17
19
|
import { moduleDoc, moduleForFile, moduleFiles } from "../definition/manifest-navigation.js";
|
|
20
|
+
import { moduleCallSites } from "../cel/module-calls.js";
|
|
18
21
|
import {
|
|
19
22
|
declarationSites,
|
|
23
|
+
functionCallSites,
|
|
20
24
|
resourceDeclarations,
|
|
21
25
|
resourceSites,
|
|
22
26
|
stepDeclarations,
|
|
@@ -87,7 +91,7 @@ export function prepareRename(
|
|
|
87
91
|
end: offsetToPosition(span[1], lineOffsets),
|
|
88
92
|
});
|
|
89
93
|
|
|
90
|
-
const symbol = symbolAt(resolved, astDocs, toRange);
|
|
94
|
+
const symbol = symbolAt(resolved, astDocs, toRange, text, ownReceivers(mod));
|
|
91
95
|
if (!symbol) return { ok: false, reason: "Nothing renameable here." };
|
|
92
96
|
if ("reason" in symbol) return { ok: false, reason: symbol.reason };
|
|
93
97
|
|
|
@@ -104,6 +108,9 @@ export function buildRename(
|
|
|
104
108
|
graph: LoadedGraph,
|
|
105
109
|
currentFilePath: string,
|
|
106
110
|
docs?: AstDocument[],
|
|
111
|
+
/** Decides whether a resource's name denotes a type (a named shape) or a
|
|
112
|
+
* value. Without it every resource name is checked as a value. */
|
|
113
|
+
analysis?: ManifestAnalysis,
|
|
107
114
|
): RenameResult {
|
|
108
115
|
const prepared = prepareRename(text, line, character, graph, currentFilePath, docs);
|
|
109
116
|
if (!prepared.ok) return prepared;
|
|
@@ -111,19 +118,20 @@ export function buildRename(
|
|
|
111
118
|
|
|
112
119
|
if (newName === symbol.name) return { ok: true, symbol, files: [] };
|
|
113
120
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
//
|
|
117
|
-
//
|
|
118
|
-
|
|
121
|
+
const mod = moduleForFile(graph, currentFilePath)!;
|
|
122
|
+
|
|
123
|
+
// The new name is checked against the half of the convention its declaration
|
|
124
|
+
// belongs to — the same rule `telo check` enforces. Renaming *into* a name the
|
|
125
|
+
// analyzer would reject is a mistake worth catching in the rename box rather
|
|
126
|
+
// than as a squiggle afterwards.
|
|
127
|
+
const violation = checkName(newName, nameLevelOf(symbol, mod, analysis), surfaceLabel(symbol));
|
|
119
128
|
if (violation) return { ok: false, reason: violation.message };
|
|
120
129
|
|
|
121
|
-
const mod = moduleForFile(graph, currentFilePath)!;
|
|
122
130
|
const files = filesForRename(symbol, mod, currentFilePath, text, docs);
|
|
123
131
|
|
|
124
132
|
const out: RenameFileEdits[] = [];
|
|
125
133
|
for (const file of files) {
|
|
126
|
-
const edits = editsIn(file, symbol, newName);
|
|
134
|
+
const edits = editsIn(file, symbol, newName, ownReceivers(mod));
|
|
127
135
|
if (edits.length > 0) out.push({ uri: file.uri, edits });
|
|
128
136
|
}
|
|
129
137
|
if (out.length === 0) {
|
|
@@ -138,6 +146,26 @@ export function buildRename(
|
|
|
138
146
|
|
|
139
147
|
type SymbolOrRefusal = RenameSymbol | { reason: string };
|
|
140
148
|
|
|
149
|
+
/** Steps and config keys are values; a resource is whatever its capability makes
|
|
150
|
+
* it — a named shape's name is a type. */
|
|
151
|
+
function nameLevelOf(symbol: RenameSymbol, mod: LoadedModule, analysis: ManifestAnalysis | undefined): NameLevel {
|
|
152
|
+
if (symbol.kind !== "resource" || !analysis) return "value";
|
|
153
|
+
const sources = new Set(moduleFiles(mod).map((file) => file.source));
|
|
154
|
+
const declaration = analysis.manifests.find(
|
|
155
|
+
(m) =>
|
|
156
|
+
m.metadata?.name === symbol.name &&
|
|
157
|
+
sources.has((m.metadata as { source?: string } | undefined)?.source ?? ""),
|
|
158
|
+
);
|
|
159
|
+
return declaration ? analysis.nameLevel(declaration) : "value";
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** The receivers a module call names a function of THIS module through: `Self`
|
|
163
|
+
* and the module's own name. */
|
|
164
|
+
function ownReceivers(mod: LoadedModule): ReadonlySet<string> {
|
|
165
|
+
const name = (moduleDoc(mod) as { metadata?: { name?: unknown } } | undefined)?.metadata?.name;
|
|
166
|
+
return new Set(typeof name === "string" && name ? ["Self", name] : ["Self"]);
|
|
167
|
+
}
|
|
168
|
+
|
|
141
169
|
const DECLARATION_BLOCKS = new Set(["variables", "secrets", "ports"]);
|
|
142
170
|
const MODULE_DOC_KINDS = new Set(["Telo.Application", "Telo.Library"]);
|
|
143
171
|
const KIND_DOC_KINDS = new Set(["Telo.Definition", "Telo.Abstract"]);
|
|
@@ -150,8 +178,10 @@ function symbolAt(
|
|
|
150
178
|
resolved: ReturnType<typeof resolveNodeAtPosition> & object,
|
|
151
179
|
astDocs: AstDocument[],
|
|
152
180
|
toRange: (span: [number, number]) => Range,
|
|
181
|
+
text: string,
|
|
182
|
+
receivers: ReadonlySet<string>,
|
|
153
183
|
): SymbolOrRefusal | undefined {
|
|
154
|
-
if (resolved.cel) return celSymbol(resolved.cel, toRange);
|
|
184
|
+
if (resolved.cel) return celSymbol(resolved.cel, toRange, text, receivers);
|
|
155
185
|
|
|
156
186
|
const path = resolved.path;
|
|
157
187
|
const node = resolved.node;
|
|
@@ -247,6 +277,8 @@ function symbolAt(
|
|
|
247
277
|
function celSymbol(
|
|
248
278
|
cel: { segment: { ast(): unknown; source: string }; offset: number },
|
|
249
279
|
toRange: (span: [number, number]) => Range,
|
|
280
|
+
text: string,
|
|
281
|
+
receivers: ReadonlySet<string>,
|
|
250
282
|
): SymbolOrRefusal | undefined {
|
|
251
283
|
let ast;
|
|
252
284
|
try {
|
|
@@ -255,6 +287,11 @@ function celSymbol(
|
|
|
255
287
|
if (!(error instanceof CelParseError)) throw error;
|
|
256
288
|
return undefined;
|
|
257
289
|
}
|
|
290
|
+
// A call of one of this module's functions renames the function resource.
|
|
291
|
+
const call = moduleCallSites(text, ast, (receiver) => receivers.has(receiver)).find(
|
|
292
|
+
(site) => cel.offset >= site.nameRange[0] && cel.offset <= site.nameRange[1],
|
|
293
|
+
);
|
|
294
|
+
if (call) return { kind: "resource", name: call.name, range: toRange(call.nameRange) };
|
|
258
295
|
const hit = chainAt(ast, cel.offset);
|
|
259
296
|
if (!hit || hit.index !== 1) return undefined;
|
|
260
297
|
const root = hit.parts[0].name;
|
|
@@ -426,7 +463,12 @@ function filesForRename(
|
|
|
426
463
|
return [];
|
|
427
464
|
}
|
|
428
465
|
|
|
429
|
-
function editsIn(
|
|
466
|
+
function editsIn(
|
|
467
|
+
file: RenameFile,
|
|
468
|
+
symbol: RenameSymbol,
|
|
469
|
+
newName: string,
|
|
470
|
+
receivers: ReadonlySet<string>,
|
|
471
|
+
): RenameEdit[] {
|
|
430
472
|
const lineOffsets = buildLineOffsets(file.text);
|
|
431
473
|
const spans: Array<[number, number]> = [];
|
|
432
474
|
|
|
@@ -434,6 +476,9 @@ function editsIn(file: RenameFile, symbol: RenameSymbol, newName: string): Renam
|
|
|
434
476
|
if (symbol.kind === "resource") {
|
|
435
477
|
for (const span of resourceDeclarations(doc, symbol.name)) spans.push(span);
|
|
436
478
|
for (const site of resourceSites(doc, symbol.name)) spans.push(site.range);
|
|
479
|
+
for (const site of functionCallSites(doc, file.text, receivers, symbol.name)) {
|
|
480
|
+
spans.push(site.range);
|
|
481
|
+
}
|
|
437
482
|
for (const span of exportEntrySpans(doc, symbol.name)) spans.push(span);
|
|
438
483
|
} else if (symbol.kind === "step") {
|
|
439
484
|
for (const span of stepDeclarations(doc, symbol.name)) spans.push(span);
|
package/src/rename/find-sites.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
} from "@telorun/analyzer";
|
|
8
8
|
|
|
9
9
|
import { walkCel } from "../cel-chain.js";
|
|
10
|
+
import { moduleCallSites } from "../cel/module-calls.js";
|
|
10
11
|
import { scalarString } from "../completions/resolve-node.js";
|
|
11
12
|
|
|
12
13
|
/**
|
|
@@ -113,6 +114,35 @@ export function resourceSites(doc: AstDocument, name: string): NameSite[] {
|
|
|
113
114
|
return sites;
|
|
114
115
|
}
|
|
115
116
|
|
|
117
|
+
/** A function's calls within one document: `<receiver>.<name>(…)` in CEL, for a
|
|
118
|
+
* receiver naming the declaring module itself (`Self`, its own name). A call
|
|
119
|
+
* through an import alias is another module's reference and is not matched.
|
|
120
|
+
* `text` is the document text the CEL offsets are measured against. */
|
|
121
|
+
export function functionCallSites(
|
|
122
|
+
doc: AstDocument,
|
|
123
|
+
text: string,
|
|
124
|
+
receivers: ReadonlySet<string>,
|
|
125
|
+
name: string,
|
|
126
|
+
): NameSite[] {
|
|
127
|
+
const sites: NameSite[] = [];
|
|
128
|
+
if (!doc.root) return sites;
|
|
129
|
+
eachScalar(doc.root, (scalar) => {
|
|
130
|
+
for (const segment of scalar.celSegments()) {
|
|
131
|
+
let ast: CelNode;
|
|
132
|
+
try {
|
|
133
|
+
ast = segment.ast();
|
|
134
|
+
} catch (error) {
|
|
135
|
+
if (!(error instanceof CelParseError)) throw error;
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
for (const site of moduleCallSites(text, ast, (receiver) => receivers.has(receiver))) {
|
|
139
|
+
if (site.name === name) sites.push({ range: site.nameRange });
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
return sites;
|
|
144
|
+
}
|
|
145
|
+
|
|
116
146
|
/** A step's references within its declaring document: `steps.<name>` in CEL.
|
|
117
147
|
* Deliberately document-scoped — `steps.<name>.result` is readable only inside
|
|
118
148
|
* the resource whose body declares the step, and a resource is one document. */
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Signature help inside a CEL body: the signature of the module call the cursor
|
|
3
|
+
* is writing the arguments of, with the argument under the cursor active.
|
|
4
|
+
*
|
|
5
|
+
* TEXTUAL, like completion's cursor chain, because it fires while the call is
|
|
6
|
+
* unfinished — `Billing.total(items, ` does not parse — and a parse-first
|
|
7
|
+
* approach would go silent exactly when help is asked for. The scan tracks
|
|
8
|
+
* brackets and string literals, so a comma inside a list or a quoted string is
|
|
9
|
+
* not an argument separator. Which receivers are modules, and what a call binds
|
|
10
|
+
* to, stay the analyzer's answers.
|
|
11
|
+
*/
|
|
12
|
+
import {
|
|
13
|
+
parseToAst,
|
|
14
|
+
type AstDocument,
|
|
15
|
+
type ManifestAnalysis,
|
|
16
|
+
} from "@telorun/analyzer";
|
|
17
|
+
import type { SignatureHelpResult } from "../types.js";
|
|
18
|
+
import { bodyStart } from "../cel/cursor-chain.js";
|
|
19
|
+
import { describeFunction, functionSignature } from "../cel/module-calls.js";
|
|
20
|
+
import { resolveNodeAtPosition } from "../completions/resolve-node.js";
|
|
21
|
+
import { docIdentity } from "../doc-identity.js";
|
|
22
|
+
|
|
23
|
+
interface Frame {
|
|
24
|
+
/** `Receiver.name` for a call's parenthesis; undefined for any other bracket. */
|
|
25
|
+
call?: { receiver: string; name: string };
|
|
26
|
+
commas: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** `Receiver.name` immediately before a `(`, where the receiver is a bare
|
|
30
|
+
* identifier — not a member of something else, which templating reads as an
|
|
31
|
+
* ordinary method call (`a.Billing.format(`). */
|
|
32
|
+
const CALLEE = /(?<![\w.])([A-Za-z_][A-Za-z0-9_]*)\.([A-Za-z_][A-Za-z0-9_]*)\s*$/;
|
|
33
|
+
|
|
34
|
+
/** The innermost bracket still open at the end of `source`. */
|
|
35
|
+
function openFrame(source: string): Frame | undefined {
|
|
36
|
+
const stack: Frame[] = [];
|
|
37
|
+
for (let i = 0; i < source.length; i++) {
|
|
38
|
+
const c = source[i]!;
|
|
39
|
+
if (c === '"' || c === "'") {
|
|
40
|
+
// Skip the literal, honouring escapes; an unterminated one runs to the end.
|
|
41
|
+
for (i++; i < source.length && source[i] !== c; i++) {
|
|
42
|
+
if (source[i] === "\\") i++;
|
|
43
|
+
}
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (c === "(") {
|
|
47
|
+
const match = CALLEE.exec(source.slice(0, i));
|
|
48
|
+
stack.push({ ...(match ? { call: { receiver: match[1]!, name: match[2]! } } : {}), commas: 0 });
|
|
49
|
+
} else if (c === "[" || c === "{") {
|
|
50
|
+
stack.push({ commas: 0 });
|
|
51
|
+
} else if (c === ")" || c === "]" || c === "}") {
|
|
52
|
+
stack.pop();
|
|
53
|
+
} else if (c === "," && stack.length > 0) {
|
|
54
|
+
stack[stack.length - 1]!.commas++;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return stack[stack.length - 1];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function buildSignatureHelp(
|
|
61
|
+
text: string,
|
|
62
|
+
line: number,
|
|
63
|
+
character: number,
|
|
64
|
+
docs: AstDocument[] | undefined,
|
|
65
|
+
analysis: ManifestAnalysis | undefined,
|
|
66
|
+
): SignatureHelpResult | undefined {
|
|
67
|
+
const query = analysis?.celScope;
|
|
68
|
+
if (!query) return undefined;
|
|
69
|
+
const astDocs = docs ?? parseToAst(text);
|
|
70
|
+
const resolved = resolveNodeAtPosition(text, astDocs, line, character);
|
|
71
|
+
if (!resolved?.cel) return undefined;
|
|
72
|
+
const { segment, offset } = resolved.cel;
|
|
73
|
+
|
|
74
|
+
const start = bodyStart(text, segment);
|
|
75
|
+
const frame = openFrame(segment.source.slice(0, Math.max(0, offset - start)));
|
|
76
|
+
if (!frame?.call) return undefined;
|
|
77
|
+
|
|
78
|
+
const identity = docIdentity(astDocs[resolved.docIndex]);
|
|
79
|
+
const resource = query.resourceFor(identity.kind, identity.name);
|
|
80
|
+
if (!resource) return undefined;
|
|
81
|
+
const scope = query.scopeAt(resource, resolved.concretePath ?? "");
|
|
82
|
+
if (!scope.moduleNames.has(frame.call.receiver)) return undefined;
|
|
83
|
+
const qualified = `${frame.call.receiver}.${frame.call.name}`;
|
|
84
|
+
const fn = scope.moduleFunction(qualified);
|
|
85
|
+
if (!fn) return undefined;
|
|
86
|
+
|
|
87
|
+
const signature = functionSignature(qualified, fn);
|
|
88
|
+
const documentation = describeFunction(fn, scope.moduleCallFlags(qualified));
|
|
89
|
+
return {
|
|
90
|
+
signatures: [
|
|
91
|
+
{
|
|
92
|
+
label: signature.label,
|
|
93
|
+
parameters: signature.parameters,
|
|
94
|
+
...(documentation ? { documentation } : {}),
|
|
95
|
+
},
|
|
96
|
+
],
|
|
97
|
+
activeSignature: 0,
|
|
98
|
+
activeParameter: Math.min(frame.commas, Math.max(0, fn.params.length - 1)),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { buildSignatureHelp } from "./build-signature-help.js";
|
package/src/types.ts
CHANGED
|
@@ -12,6 +12,7 @@ export type {
|
|
|
12
12
|
|
|
13
13
|
import type {
|
|
14
14
|
AnalysisRegistry,
|
|
15
|
+
DiagnosticFixTag,
|
|
15
16
|
DiagnosticSeverity,
|
|
16
17
|
DiagnosticTag,
|
|
17
18
|
Position,
|
|
@@ -53,6 +54,19 @@ export interface HoverResult {
|
|
|
53
54
|
range?: ReplaceRange;
|
|
54
55
|
}
|
|
55
56
|
|
|
57
|
+
/** Signature help for the call the cursor is inside. Each parameter's `label` is
|
|
58
|
+
* its `[start, end]` span inside the signature's own `label`, the form both
|
|
59
|
+
* VS Code and Monaco highlight the active parameter by. */
|
|
60
|
+
export interface SignatureHelpResult {
|
|
61
|
+
signatures: Array<{
|
|
62
|
+
label: string;
|
|
63
|
+
documentation?: string;
|
|
64
|
+
parameters: Array<{ label: [number, number]; documentation?: string }>;
|
|
65
|
+
}>;
|
|
66
|
+
activeSignature: number;
|
|
67
|
+
activeParameter: number;
|
|
68
|
+
}
|
|
69
|
+
|
|
56
70
|
/** Semantic token type names emitted by `buildSemanticTokens`. Kept to the
|
|
57
71
|
* standard VS Code / LSP set so hosts register them against a stock legend and
|
|
58
72
|
* every theme colors them without extra configuration.
|
|
@@ -167,8 +181,9 @@ export interface NormalizedDiagnostic {
|
|
|
167
181
|
source: string;
|
|
168
182
|
message: string;
|
|
169
183
|
/** Mechanically applicable repairs. `replacement` is the whole corrected
|
|
170
|
-
* value at the diagnostic's range — apply it by replacing that range
|
|
171
|
-
|
|
184
|
+
* value at the diagnostic's range — apply it by replacing that range,
|
|
185
|
+
* rendered through `renderFixReplacement` with `tag` when one is present. */
|
|
186
|
+
suggestions?: Array<{ kind: "replace"; replacement: string; tag?: DiagnosticFixTag }>;
|
|
172
187
|
/** LSP diagnostic tags, carried through verbatim. Orthogonal to `severity`:
|
|
173
188
|
* they say what KIND of thing the range is (deprecated, unnecessary), which
|
|
174
189
|
* is what a host renders as strikethrough or fading rather than as a colour. */
|