@telorun/ide-support 0.6.0 → 0.7.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/completions/build.d.ts +2 -2
- package/dist/completions/build.d.ts.map +1 -1
- package/dist/completions/build.js +39 -64
- package/dist/completions/detect-context.d.ts +16 -43
- package/dist/completions/detect-context.d.ts.map +1 -1
- package/dist/completions/detect-context.js +63 -262
- package/dist/completions/import-source.d.ts +5 -5
- package/dist/completions/import-source.d.ts.map +1 -1
- package/dist/completions/import-source.js +15 -15
- package/dist/completions/resolve-node.d.ts +50 -0
- package/dist/completions/resolve-node.d.ts.map +1 -0
- package/dist/completions/resolve-node.js +269 -0
- package/dist/completions/valid-capabilities.d.ts +3 -0
- package/dist/completions/valid-capabilities.d.ts.map +1 -1
- package/dist/completions/valid-capabilities.js +10 -0
- package/dist/definition/build-definition.d.ts +13 -0
- package/dist/definition/build-definition.d.ts.map +1 -0
- package/dist/definition/build-definition.js +98 -0
- package/dist/definition/index.d.ts +2 -0
- package/dist/definition/index.d.ts.map +1 -0
- package/dist/definition/index.js +1 -0
- package/dist/hover/build-hover.d.ts +4 -0
- package/dist/hover/build-hover.d.ts.map +1 -0
- package/dist/hover/build-hover.js +125 -0
- package/dist/hover/index.d.ts +2 -0
- package/dist/hover/index.d.ts.map +1 -0
- package/dist/hover/index.js +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/semantic-tokens/build-semantic-tokens.d.ts +12 -0
- package/dist/semantic-tokens/build-semantic-tokens.d.ts.map +1 -0
- package/dist/semantic-tokens/build-semantic-tokens.js +55 -0
- package/dist/semantic-tokens/index.d.ts +2 -0
- package/dist/semantic-tokens/index.d.ts.map +1 -0
- package/dist/semantic-tokens/index.js +1 -0
- package/dist/types.d.ts +45 -6
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +3 -0
- package/package.json +2 -2
- package/src/completions/build.ts +40 -72
- package/src/completions/detect-context.ts +74 -291
- package/src/completions/import-source.ts +16 -16
- package/src/completions/resolve-node.ts +405 -0
- package/src/completions/valid-capabilities.ts +11 -0
- package/src/definition/build-definition.ts +123 -0
- package/src/definition/index.ts +1 -0
- package/src/hover/build-hover.ts +149 -0
- package/src/hover/index.ts +1 -0
- package/src/index.ts +3 -0
- package/src/semantic-tokens/build-semantic-tokens.ts +64 -0
- package/src/semantic-tokens/index.ts +1 -0
- package/src/types.ts +57 -6
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
import { parseToAst, type AstDocument } from "@telorun/analyzer";
|
|
2
|
+
import type { ReplaceRange } from "../types.js";
|
|
3
|
+
import { resolveNodeAtPosition } from "./resolve-node.js";
|
|
4
|
+
|
|
5
|
+
export type { ReplaceRange };
|
|
6
|
+
|
|
1
7
|
export type CompletionCtx =
|
|
2
8
|
| {
|
|
3
9
|
type: "kind";
|
|
@@ -7,12 +13,10 @@ export type CompletionCtx =
|
|
|
7
13
|
* Absent for top-level `kind:` — there, no constraint applies. */
|
|
8
14
|
docKind?: string;
|
|
9
15
|
yamlPath?: string[];
|
|
10
|
-
/**
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
|
|
14
|
-
* which VS Code's default word range would). */
|
|
15
|
-
valueStartColumn: number;
|
|
16
|
+
/** Full source range of the kind value, so a pick overwrites the whole
|
|
17
|
+
* existing scalar (e.g. `Sql.Co|nnection` + `Sql.Connection` → no
|
|
18
|
+
* suffix left behind). */
|
|
19
|
+
replaceRange: ReplaceRange;
|
|
16
20
|
}
|
|
17
21
|
| { type: "capability" }
|
|
18
22
|
| { type: "prop-key"; docKind: string; yamlPath: string[]; existingKeys: Set<string> }
|
|
@@ -29,7 +33,7 @@ export type CompletionCtx =
|
|
|
29
33
|
/** The kind value of the sibling `kind:` line, if present. */
|
|
30
34
|
refKind?: string;
|
|
31
35
|
prefix: string;
|
|
32
|
-
|
|
36
|
+
replaceRange: ReplaceRange;
|
|
33
37
|
}
|
|
34
38
|
| {
|
|
35
39
|
type: "field-value";
|
|
@@ -37,141 +41,10 @@ export type CompletionCtx =
|
|
|
37
41
|
field: string;
|
|
38
42
|
/** Text from the start of the value to the cursor. */
|
|
39
43
|
prefix: string;
|
|
40
|
-
/**
|
|
41
|
-
|
|
44
|
+
/** Full source range of the value being completed. */
|
|
45
|
+
replaceRange: ReplaceRange;
|
|
42
46
|
};
|
|
43
47
|
|
|
44
|
-
export function findDocBounds(lines: string[], cursorLine: number): { start: number; end: number } {
|
|
45
|
-
let start = 0;
|
|
46
|
-
for (let i = cursorLine; i >= 0; i--) {
|
|
47
|
-
if (lines[i]?.trimEnd() === "---") {
|
|
48
|
-
start = i + 1;
|
|
49
|
-
break;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
let end = lines.length;
|
|
53
|
-
for (let i = cursorLine + 1; i < lines.length; i++) {
|
|
54
|
-
if (lines[i]?.trimEnd() === "---") {
|
|
55
|
-
end = i;
|
|
56
|
-
break;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
return { start, end };
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export function extractKindFromDoc(lines: string[], start: number, end: number): string | undefined {
|
|
63
|
-
for (let i = start; i < end; i++) {
|
|
64
|
-
const m = lines[i]?.match(/^kind:\s*(\S+)/);
|
|
65
|
-
if (m) return m[1];
|
|
66
|
-
}
|
|
67
|
-
return undefined;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/** Collects every top-level key in the doc bounds, skipping `skipLine` so the
|
|
71
|
-
* cursor's own line is treated as "being edited" — its key (if any) stays in
|
|
72
|
-
* the suggestion list. Without this the user can't autocomplete an existing
|
|
73
|
-
* key from its own line (e.g. `ver|sion:`). */
|
|
74
|
-
export function extractRootKeys(
|
|
75
|
-
lines: string[],
|
|
76
|
-
start: number,
|
|
77
|
-
end: number,
|
|
78
|
-
skipLine?: number,
|
|
79
|
-
): Set<string> {
|
|
80
|
-
const keys = new Set<string>();
|
|
81
|
-
for (let i = start; i < end; i++) {
|
|
82
|
-
if (i === skipLine) continue;
|
|
83
|
-
const m = lines[i]?.match(/^([a-zA-Z_][a-zA-Z0-9_]*):/);
|
|
84
|
-
if (m) keys.add(m[1]);
|
|
85
|
-
}
|
|
86
|
-
return keys;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/** Walk backward from cursorLine to build the chain of parent YAML keys.
|
|
90
|
-
*
|
|
91
|
-
* List-item handling (` - request:` style): the `-` marker sits at the
|
|
92
|
-
* line's textual indent, but the key after it (`request`) lives at indent
|
|
93
|
-
* `+2`. Whether that post-dash key joins the path depends on the cursor's
|
|
94
|
-
* descent:
|
|
95
|
-
* - When the cursor's current target indent is GREATER than the post-dash
|
|
96
|
-
* key's column, the descent passes through that key (e.g. cursor inside
|
|
97
|
-
* `request.method` at indent 6, key `request` at column 4) → push it.
|
|
98
|
-
* - When the cursor's current target indent EQUALS the post-dash key's
|
|
99
|
-
* column, the post-dash key is a sibling at the list-item level
|
|
100
|
-
* (e.g. cursor on `handler:` at indent 4, key `request:` at column 4) →
|
|
101
|
-
* skip it; descend straight to the array's parent.
|
|
102
|
-
*
|
|
103
|
-
* In both cases the next walk step targets `lineIndent` so the `routes:` /
|
|
104
|
-
* `steps:` parent of the array is captured. The schema walker auto-descends
|
|
105
|
-
* arrays, so no `[]` marker is appended. */
|
|
106
|
-
export function buildYamlPath(
|
|
107
|
-
lines: string[],
|
|
108
|
-
cursorLine: number,
|
|
109
|
-
docStart: number,
|
|
110
|
-
cursorIndent: number,
|
|
111
|
-
): string[] {
|
|
112
|
-
if (cursorIndent === 0) return [];
|
|
113
|
-
|
|
114
|
-
const path: string[] = [];
|
|
115
|
-
let targetIndent = cursorIndent;
|
|
116
|
-
|
|
117
|
-
for (let i = cursorLine - 1; i >= docStart; i--) {
|
|
118
|
-
const line = lines[i] ?? "";
|
|
119
|
-
const trimmed = line.trimStart();
|
|
120
|
-
if (trimmed === "" || trimmed.startsWith("#")) continue;
|
|
121
|
-
|
|
122
|
-
const lineIndent = line.length - trimmed.length;
|
|
123
|
-
if (lineIndent < targetIndent) {
|
|
124
|
-
// Match a plain object key (not a list item marker)
|
|
125
|
-
const m = trimmed.match(/^([a-zA-Z_][a-zA-Z0-9_]*):/);
|
|
126
|
-
if (m) {
|
|
127
|
-
path.unshift(m[1]);
|
|
128
|
-
targetIndent = lineIndent;
|
|
129
|
-
if (lineIndent === 0) break;
|
|
130
|
-
} else if (trimmed.startsWith("- ")) {
|
|
131
|
-
const postDash = trimmed.slice(2);
|
|
132
|
-
const km = postDash.match(/^([a-zA-Z_][a-zA-Z0-9_]*):/);
|
|
133
|
-
const keyColumn = lineIndent + 2;
|
|
134
|
-
if (km && keyColumn < targetIndent) {
|
|
135
|
-
path.unshift(km[1]);
|
|
136
|
-
}
|
|
137
|
-
targetIndent = lineIndent;
|
|
138
|
-
} else if (trimmed === "-") {
|
|
139
|
-
targetIndent = lineIndent;
|
|
140
|
-
} else {
|
|
141
|
-
// Hit something we can't parse; stop
|
|
142
|
-
break;
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
return path;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/** Extract sibling keys already present at `indent` within the doc bounds.
|
|
151
|
-
* `skipLine` lets the caller exclude the cursor's own line so a key being
|
|
152
|
-
* edited (`ver|sion:`) doesn't filter itself out of the suggestion list. */
|
|
153
|
-
export function extractKeysAtIndent(
|
|
154
|
-
lines: string[],
|
|
155
|
-
start: number,
|
|
156
|
-
end: number,
|
|
157
|
-
indent: number,
|
|
158
|
-
skipLine?: number,
|
|
159
|
-
): Set<string> {
|
|
160
|
-
const keys = new Set<string>();
|
|
161
|
-
const prefix = " ".repeat(indent);
|
|
162
|
-
for (let i = start; i < end; i++) {
|
|
163
|
-
if (i === skipLine) continue;
|
|
164
|
-
const line = lines[i] ?? "";
|
|
165
|
-
if (!line.startsWith(prefix)) continue;
|
|
166
|
-
const rest = line.slice(indent);
|
|
167
|
-
const m = rest.match(/^([a-zA-Z_][a-zA-Z0-9_]*):/);
|
|
168
|
-
if (m && line.length - line.trimStart().length === indent) {
|
|
169
|
-
keys.add(m[1]);
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
return keys;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
48
|
/** Returns every schema branch reachable from `node` after peeling `anyOf` /
|
|
176
49
|
* `oneOf` recursively. A branch with no combinators is its own only entry.
|
|
177
50
|
* Used so an `x-telo-ref` slot like `{anyOf: [{type: string}, {type: object,
|
|
@@ -272,36 +145,6 @@ function unionLeaves(
|
|
|
272
145
|
* completion to discover what kind of resource the user is targeting in an
|
|
273
146
|
* object-form ref. Walking stops at the first line with a strictly smaller
|
|
274
147
|
* indent (that's the parent's structural boundary). */
|
|
275
|
-
export function findSiblingKindValue(
|
|
276
|
-
lines: string[],
|
|
277
|
-
docStart: number,
|
|
278
|
-
docEnd: number,
|
|
279
|
-
cursorLine: number,
|
|
280
|
-
indent: number,
|
|
281
|
-
): string | undefined {
|
|
282
|
-
const prefix = " ".repeat(indent);
|
|
283
|
-
const scan = (range: number[]): string | undefined => {
|
|
284
|
-
for (const i of range) {
|
|
285
|
-
const line = lines[i] ?? "";
|
|
286
|
-
if (line.trim() === "" || line.trim().startsWith("#")) continue;
|
|
287
|
-
if (line.trimEnd() === "---") return undefined;
|
|
288
|
-
const lineIndent = line.length - line.trimStart().length;
|
|
289
|
-
if (lineIndent < indent) return undefined; // parent boundary
|
|
290
|
-
if (lineIndent !== indent || !line.startsWith(prefix)) continue;
|
|
291
|
-
const m = line.slice(indent).match(/^kind:\s*(\S+)/);
|
|
292
|
-
if (m) return m[1];
|
|
293
|
-
}
|
|
294
|
-
return undefined;
|
|
295
|
-
};
|
|
296
|
-
// Forward then backward — order doesn't matter for correctness because
|
|
297
|
-
// any sibling kind value at this indent applies to the same object.
|
|
298
|
-
const after = [];
|
|
299
|
-
for (let i = cursorLine + 1; i < docEnd; i++) after.push(i);
|
|
300
|
-
const before = [];
|
|
301
|
-
for (let i = cursorLine - 1; i >= docStart; i--) before.push(i);
|
|
302
|
-
return scan(after) ?? scan(before);
|
|
303
|
-
}
|
|
304
|
-
|
|
305
148
|
/** Looks up the `x-telo-ref` string carried by the schema node at `yamlPath`
|
|
306
149
|
* inside `definitionSchema`. Checks both the property node directly and its
|
|
307
150
|
* peeled `anyOf` / `oneOf` branches, since some library schemas place the
|
|
@@ -320,144 +163,84 @@ export function lookupRefConstraint(
|
|
|
320
163
|
return undefined;
|
|
321
164
|
}
|
|
322
165
|
|
|
166
|
+
/** Derive a `CompletionCtx` from the AST-resolved cursor (Approach B). The
|
|
167
|
+
* structural resolution lives in `resolveNodeAtPosition`; this only maps a
|
|
168
|
+
* resolved slot onto the completion the editor should offer. `docs` lets a
|
|
169
|
+
* host thread its already-parsed AST; without it we parse locally so this
|
|
170
|
+
* stands alone. */
|
|
323
171
|
export function detectContext(
|
|
324
172
|
text: string,
|
|
325
173
|
line: number,
|
|
326
174
|
character: number,
|
|
175
|
+
docs?: AstDocument[],
|
|
327
176
|
): CompletionCtx | undefined {
|
|
328
|
-
const
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
if (
|
|
345
|
-
|
|
346
|
-
const yamlPath = buildYamlPath(lines, line, start, indent);
|
|
347
|
-
return { type: "kind", docKind, yamlPath, valueStartColumn: valueStart };
|
|
348
|
-
}
|
|
177
|
+
const resolved = resolveNodeAtPosition(text, docs ?? parseToAst(text), line, character);
|
|
178
|
+
if (!resolved) return undefined;
|
|
179
|
+
const { docKind } = resolved;
|
|
180
|
+
|
|
181
|
+
if (resolved.slot === "value") {
|
|
182
|
+
// Inside a CEL body — structural completion does not apply (a future
|
|
183
|
+
// CEL-completion feature consumes `resolved.cel`).
|
|
184
|
+
if (resolved.cel) return undefined;
|
|
185
|
+
const replaceRange = resolved.replaceRange;
|
|
186
|
+
if (!replaceRange) return undefined;
|
|
187
|
+
const key = resolved.path[resolved.path.length - 1];
|
|
188
|
+
const parentPath = resolved.path.slice(0, -1);
|
|
189
|
+
const prefix = resolved.prefix ?? "";
|
|
190
|
+
|
|
191
|
+
if (key === "kind") {
|
|
192
|
+
if (parentPath.length === 0) return { type: "kind", replaceRange };
|
|
193
|
+
if (docKind) return { type: "kind", docKind, yamlPath: parentPath, replaceRange };
|
|
194
|
+
return undefined;
|
|
349
195
|
}
|
|
350
|
-
// Cursor is on the key portion — fall through to prop-key handling.
|
|
351
|
-
}
|
|
352
196
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
}
|
|
197
|
+
if (key === "capability" && docKind === "Telo.Definition") {
|
|
198
|
+
return { type: "capability" };
|
|
199
|
+
}
|
|
357
200
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
// will fall back to filtering by `refKind` regardless of the schema. Doing
|
|
363
|
-
// so keeps editor autocomplete working even when the registry hasn't fully
|
|
364
|
-
// resolved the resource's definition.
|
|
365
|
-
const nameLineMatch = currentLine.match(/^(\s+)name:(\s*)(\S*)$/);
|
|
366
|
-
if (nameLineMatch && docKind) {
|
|
367
|
-
const indent = nameLineMatch[1].length;
|
|
368
|
-
const valueStart = indent + "name:".length + nameLineMatch[2].length;
|
|
369
|
-
const valuePrefix = nameLineMatch[3];
|
|
370
|
-
if (character >= valueStart) {
|
|
371
|
-
const yamlPath = buildYamlPath(lines, line, start, indent);
|
|
372
|
-
// The yamlPath built here points at the parent slot — for
|
|
373
|
-
// `connection: { kind: …, name: | }` the path is `["connection"]`.
|
|
374
|
-
// The sibling `kind:` lives at the same indent as our `name:`, so we
|
|
375
|
-
// scan the doc bounds for it.
|
|
376
|
-
const refKind = findSiblingKindValue(lines, start, end, line, indent);
|
|
201
|
+
// Any `name:` value is a candidate ref target; the sibling `kind:` (when
|
|
202
|
+
// present) narrows the in-file resource list. Harmless on `metadata.name`,
|
|
203
|
+
// where no ref constraint resolves and the list is the fallback.
|
|
204
|
+
if (key === "name" && docKind) {
|
|
377
205
|
return {
|
|
378
206
|
type: "ref-name",
|
|
379
207
|
docKind,
|
|
380
|
-
yamlPath,
|
|
381
|
-
refKind,
|
|
382
|
-
prefix
|
|
383
|
-
|
|
208
|
+
yamlPath: parentPath,
|
|
209
|
+
refKind: resolved.siblingKind,
|
|
210
|
+
prefix,
|
|
211
|
+
replaceRange,
|
|
384
212
|
};
|
|
385
213
|
}
|
|
386
|
-
}
|
|
387
214
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
// matches: `\S*` can't span the spaces inside the braces.
|
|
401
|
-
const entryMatch = currentLine.match(/^(\s+)([A-Za-z_][\w-]*):(\s+)(\S*)$/);
|
|
402
|
-
if (entryMatch) {
|
|
403
|
-
const indent = entryMatch[1].length;
|
|
404
|
-
const key = entryMatch[2];
|
|
405
|
-
const valueStartColumn = indent + key.length + 1 + entryMatch[3].length;
|
|
406
|
-
if (character >= valueStartColumn) {
|
|
407
|
-
const parentPath = buildYamlPath(lines, line, start, indent);
|
|
408
|
-
const isScalarEntry = parentPath.length === 1 && parentPath[0] === "imports";
|
|
409
|
-
const isObjectSource =
|
|
410
|
-
key === "source" && parentPath.length === 2 && parentPath[0] === "imports";
|
|
411
|
-
if (isScalarEntry || isObjectSource) {
|
|
412
|
-
const prefix = currentLine.slice(valueStartColumn, character);
|
|
413
|
-
return { type: "field-value", docKind, field: "import-source", prefix, valueStartColumn };
|
|
414
|
-
}
|
|
215
|
+
// Import-source: the scalar shorthand `imports.<Alias>` or the object-form
|
|
216
|
+
// `imports.<Alias>.source`. `spaceAfterColon` distinguishes `Console: ` (a
|
|
217
|
+
// value) from a bare `Tiny:` header about to carry a nested `source:`.
|
|
218
|
+
if (
|
|
219
|
+
(docKind === "Telo.Application" || docKind === "Telo.Library") &&
|
|
220
|
+
resolved.spaceAfterColon
|
|
221
|
+
) {
|
|
222
|
+
const isScalarEntry = parentPath.length === 1 && parentPath[0] === "imports";
|
|
223
|
+
const isObjectSource =
|
|
224
|
+
key === "source" && parentPath.length === 2 && parentPath[0] === "imports";
|
|
225
|
+
if (isScalarEntry || isObjectSource) {
|
|
226
|
+
return { type: "field-value", docKind, field: "import-source", prefix, replaceRange };
|
|
415
227
|
}
|
|
416
228
|
}
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
const trimmed = currentLine.trim();
|
|
420
229
|
|
|
421
|
-
|
|
422
|
-
// 1. Blank line / whitespace only — `existingKeys` skip means the user is
|
|
423
|
-
// starting a fresh key.
|
|
424
|
-
// 2. Line has no `:` yet (e.g. `vers`, `version`) — partial key being typed.
|
|
425
|
-
// 3. Line has `key: value` and the cursor is at or before the colon.
|
|
426
|
-
// The line text is preserved so `version|: 1.0.0` keeps suggesting keys
|
|
427
|
-
// while `version: |1.0.0` falls through to no completion.
|
|
428
|
-
const colonIdx = currentLine.indexOf(":");
|
|
429
|
-
const beforeColon = colonIdx === -1 ? currentLine : currentLine.slice(0, colonIdx);
|
|
430
|
-
const isKeyLine =
|
|
431
|
-
trimmed === "" ||
|
|
432
|
-
(colonIdx === -1 && /^\s*[a-zA-Z_][a-zA-Z0-9_]*$/.test(currentLine)) ||
|
|
433
|
-
(colonIdx !== -1 &&
|
|
434
|
-
character <= colonIdx &&
|
|
435
|
-
/^\s*[a-zA-Z_][a-zA-Z0-9_]*\s*$/.test(beforeColon));
|
|
436
|
-
if (!isKeyLine) return undefined;
|
|
437
|
-
|
|
438
|
-
// Indent resolution: for a whitespace-only line the cursor's column tells
|
|
439
|
-
// us exactly where the user is about to type — VS Code parks the cursor at
|
|
440
|
-
// the new auto-indent after Enter, and any deviation (backspace to col 0,
|
|
441
|
-
// type extra spaces) is intentional. Trusting `character` also lets the
|
|
442
|
-
// user reach root level (col 0) on a trailing blank line even when the
|
|
443
|
-
// previous non-blank line was nested.
|
|
444
|
-
const indent =
|
|
445
|
-
trimmed === ""
|
|
446
|
-
? character
|
|
447
|
-
: currentLine.length - currentLine.trimStart().length;
|
|
448
|
-
|
|
449
|
-
if (indent === 0) {
|
|
450
|
-
return {
|
|
451
|
-
type: "prop-key",
|
|
452
|
-
docKind,
|
|
453
|
-
yamlPath: [],
|
|
454
|
-
existingKeys: extractRootKeys(lines, start, end, line),
|
|
455
|
-
};
|
|
230
|
+
return undefined;
|
|
456
231
|
}
|
|
457
232
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
233
|
+
// Key position (existing key, blank line, or trailing indent). Complete
|
|
234
|
+
// against the nearest enclosing inline resource's schema (or the root
|
|
235
|
+
// resource), with the path made relative to it — so a prop key inside
|
|
236
|
+
// `mount: { kind: Crud.Resource, … }` offers Crud.Resource's fields, not the
|
|
237
|
+
// outer ref slot's.
|
|
238
|
+
const scopeKind = resolved.resourceKind ?? docKind;
|
|
239
|
+
if (!scopeKind) return undefined;
|
|
240
|
+
return {
|
|
241
|
+
type: "prop-key",
|
|
242
|
+
docKind: scopeKind,
|
|
243
|
+
yamlPath: resolved.path.slice(resolved.resourceDepth ?? 0),
|
|
244
|
+
existingKeys: resolved.existingKeys ?? new Set<string>(),
|
|
245
|
+
};
|
|
463
246
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CompletionResult, IdeEnvironmentAdapter } from "../types.js";
|
|
1
|
+
import type { CompletionResult, IdeEnvironmentAdapter, ReplaceRange } from "../types.js";
|
|
2
2
|
|
|
3
3
|
/** Maximum ref hits to surface in a single completion request. Keeps the
|
|
4
4
|
* popover scannable when a broad `q=` query matches many registered refs
|
|
@@ -26,13 +26,13 @@ const PATH_PROBE_LIMIT = 50;
|
|
|
26
26
|
* search below, whose query is the whole typed prefix — so the hub fuzzy-matches
|
|
27
27
|
* `oci://ghcr.io/aws/telo-s3` as readily as a bare `s3`.
|
|
28
28
|
*
|
|
29
|
-
* `
|
|
30
|
-
*
|
|
31
|
-
*
|
|
29
|
+
* `replaceRange` is forwarded onto every result so the host replaces the whole
|
|
30
|
+
* value, not just the trailing word (Monaco / VSCode word boundaries don't
|
|
31
|
+
* cross `/` or `@`).
|
|
32
32
|
*/
|
|
33
33
|
export async function importSourceCompletions(
|
|
34
34
|
prefix: string,
|
|
35
|
-
|
|
35
|
+
replaceRange: ReplaceRange,
|
|
36
36
|
adapter: IdeEnvironmentAdapter | undefined,
|
|
37
37
|
): Promise<CompletionResult[]> {
|
|
38
38
|
if (!adapter) return [];
|
|
@@ -48,22 +48,22 @@ export async function importSourceCompletions(
|
|
|
48
48
|
const isRelativeShape =
|
|
49
49
|
prefix === "" || prefix.startsWith(".") || prefix.startsWith("/");
|
|
50
50
|
if (isRelativeShape) {
|
|
51
|
-
return relativePathCompletions(prefix,
|
|
51
|
+
return relativePathCompletions(prefix, replaceRange, adapter);
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
// The version (or `@sha256:` digest) is the trailing `@`-segment, so split on
|
|
55
55
|
// the LAST `@` — a digest-pinned ref keeps everything before it as the ref.
|
|
56
56
|
const atIdx = prefix.lastIndexOf("@");
|
|
57
57
|
if (atIdx > 0) {
|
|
58
|
-
return versionCompletions(prefix, atIdx,
|
|
58
|
+
return versionCompletions(prefix, atIdx, replaceRange, adapter);
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
return refSearchCompletions(prefix,
|
|
61
|
+
return refSearchCompletions(prefix, replaceRange, adapter);
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
async function relativePathCompletions(
|
|
65
65
|
prefix: string,
|
|
66
|
-
|
|
66
|
+
replaceRange: ReplaceRange,
|
|
67
67
|
adapter: IdeEnvironmentAdapter,
|
|
68
68
|
): Promise<CompletionResult[]> {
|
|
69
69
|
// Empty prefix → seed `./` and `../` so the user gets traction; otherwise
|
|
@@ -76,14 +76,14 @@ async function relativePathCompletions(
|
|
|
76
76
|
kind: "folder",
|
|
77
77
|
insertText: "./",
|
|
78
78
|
sortText: "0_./",
|
|
79
|
-
|
|
79
|
+
replaceRange,
|
|
80
80
|
},
|
|
81
81
|
{
|
|
82
82
|
label: "../",
|
|
83
83
|
kind: "folder",
|
|
84
84
|
insertText: "../",
|
|
85
85
|
sortText: "0_../",
|
|
86
|
-
|
|
86
|
+
replaceRange,
|
|
87
87
|
},
|
|
88
88
|
];
|
|
89
89
|
}
|
|
@@ -118,7 +118,7 @@ async function relativePathCompletions(
|
|
|
118
118
|
detail: isModule ? "telo module" : "folder",
|
|
119
119
|
insertText: fullPath,
|
|
120
120
|
filterText: fullPath,
|
|
121
|
-
|
|
121
|
+
replaceRange,
|
|
122
122
|
// Modules sort above plain folders so they surface first when the user
|
|
123
123
|
// is browsing a `modules/` tree mixed with non-Telo siblings.
|
|
124
124
|
sortText: isModule ? `0_${name}` : `1_${name}`,
|
|
@@ -129,7 +129,7 @@ async function relativePathCompletions(
|
|
|
129
129
|
|
|
130
130
|
async function refSearchCompletions(
|
|
131
131
|
prefix: string,
|
|
132
|
-
|
|
132
|
+
replaceRange: ReplaceRange,
|
|
133
133
|
adapter: IdeEnvironmentAdapter,
|
|
134
134
|
): Promise<CompletionResult[]> {
|
|
135
135
|
// The whole typed prefix is the fuzzy query — the hub matches it as a
|
|
@@ -154,7 +154,7 @@ async function refSearchCompletions(
|
|
|
154
154
|
documentation: m.description ? m.ref : undefined,
|
|
155
155
|
insertText: id,
|
|
156
156
|
filterText: id,
|
|
157
|
-
|
|
157
|
+
replaceRange,
|
|
158
158
|
};
|
|
159
159
|
});
|
|
160
160
|
}
|
|
@@ -173,7 +173,7 @@ function refDisplayName(ref: string): string {
|
|
|
173
173
|
async function versionCompletions(
|
|
174
174
|
prefix: string,
|
|
175
175
|
atIdx: number,
|
|
176
|
-
|
|
176
|
+
replaceRange: ReplaceRange,
|
|
177
177
|
adapter: IdeEnvironmentAdapter,
|
|
178
178
|
): Promise<CompletionResult[]> {
|
|
179
179
|
const ref = prefix.slice(0, atIdx);
|
|
@@ -195,7 +195,7 @@ async function versionCompletions(
|
|
|
195
195
|
detail: idx === 0 ? "latest" : undefined,
|
|
196
196
|
insertText: id,
|
|
197
197
|
filterText: id,
|
|
198
|
-
|
|
198
|
+
replaceRange,
|
|
199
199
|
// Preserve the hub's ordering (newest first) so the latest version is
|
|
200
200
|
// suggested at the top regardless of lexical comparison.
|
|
201
201
|
sortText: String(idx).padStart(4, "0"),
|