@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.
Files changed (51) hide show
  1. package/dist/cel/cursor-chain.d.ts +4 -0
  2. package/dist/cel/cursor-chain.d.ts.map +1 -1
  3. package/dist/cel/cursor-chain.js +1 -1
  4. package/dist/cel/module-calls.d.ts +56 -0
  5. package/dist/cel/module-calls.d.ts.map +1 -0
  6. package/dist/cel/module-calls.js +98 -0
  7. package/dist/cel/tokens.d.ts.map +1 -1
  8. package/dist/cel/tokens.js +9 -1
  9. package/dist/completions/cel-completions.d.ts.map +1 -1
  10. package/dist/completions/cel-completions.js +15 -0
  11. package/dist/completions/valid-capabilities.d.ts +1 -1
  12. package/dist/completions/valid-capabilities.d.ts.map +1 -1
  13. package/dist/completions/valid-capabilities.js +4 -0
  14. package/dist/definition/build-definition.d.ts.map +1 -1
  15. package/dist/definition/build-definition.js +46 -2
  16. package/dist/diagnostics/normalize.d.ts.map +1 -1
  17. package/dist/diagnostics/normalize.js +9 -1
  18. package/dist/hover/build-hover.d.ts.map +1 -1
  19. package/dist/hover/build-hover.js +17 -3
  20. package/dist/index.d.ts +2 -0
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +4 -0
  23. package/dist/rename/build-rename.d.ts +5 -2
  24. package/dist/rename/build-rename.d.ts.map +1 -1
  25. package/dist/rename/build-rename.js +41 -14
  26. package/dist/rename/find-sites.d.ts +5 -0
  27. package/dist/rename/find-sites.d.ts.map +1 -1
  28. package/dist/rename/find-sites.js +28 -0
  29. package/dist/signature-help/build-signature-help.d.ts +15 -0
  30. package/dist/signature-help/build-signature-help.d.ts.map +1 -0
  31. package/dist/signature-help/build-signature-help.js +87 -0
  32. package/dist/signature-help/index.d.ts +2 -0
  33. package/dist/signature-help/index.d.ts.map +1 -0
  34. package/dist/signature-help/index.js +1 -0
  35. package/dist/types.d.ts +19 -2
  36. package/dist/types.d.ts.map +1 -1
  37. package/package.json +2 -2
  38. package/src/cel/cursor-chain.ts +1 -1
  39. package/src/cel/module-calls.ts +138 -0
  40. package/src/cel/tokens.ts +8 -1
  41. package/src/completions/cel-completions.ts +15 -0
  42. package/src/completions/valid-capabilities.ts +6 -0
  43. package/src/definition/build-definition.ts +49 -1
  44. package/src/diagnostics/normalize.ts +9 -1
  45. package/src/hover/build-hover.ts +16 -2
  46. package/src/index.ts +4 -0
  47. package/src/rename/build-rename.ts +55 -10
  48. package/src/rename/find-sites.ts +30 -0
  49. package/src/signature-help/build-signature-help.ts +100 -0
  50. package/src/signature-help/index.ts +1 -0
  51. 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
- // Every renameable surface is value-level, so the new name is checked against
115
- // that half of the convention — the same rule `telo check` enforces. Renaming
116
- // *into* a name the analyzer would reject is a mistake worth catching in the
117
- // rename box rather than as a squiggle afterwards.
118
- const violation = checkName(newName, "value", surfaceLabel(symbol));
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(file: RenameFile, symbol: RenameSymbol, newName: string): RenameEdit[] {
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);
@@ -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
- suggestions?: Array<{ kind: "replace"; replacement: string }>;
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. */