@telorun/ide-support 0.23.0 → 0.24.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/src/types.ts CHANGED
@@ -20,7 +20,15 @@ import type {
20
20
  Range,
21
21
  } from "@telorun/analyzer";
22
22
 
23
- export type CompletionKind = "class" | "enumMember" | "property" | "folder" | "module" | "value";
23
+ export type CompletionKind =
24
+ | "class"
25
+ | "enumMember"
26
+ | "property"
27
+ | "folder"
28
+ | "file"
29
+ | "module"
30
+ | "value"
31
+ | "keyword";
24
32
 
25
33
  /** A source span the host replaces wholesale when a completion is accepted. */
26
34
  export interface ReplaceRange {
@@ -45,6 +53,9 @@ export interface CompletionResult {
45
53
  * containing non-word characters (`/`, `@`, `.`). A zero-width range is a
46
54
  * pure insert. */
47
55
  replaceRange?: ReplaceRange;
56
+ /** The host reopens completion once this item is accepted — what comes next
57
+ * has completions of its own (a tag's value, a directory's entries). */
58
+ retrigger?: boolean;
48
59
  }
49
60
 
50
61
  /** Rendered hover for the symbol under the cursor. `contents` is GitHub-flavored
@@ -172,6 +183,16 @@ export interface IdeEnvironmentAdapter {
172
183
  * (`GET /module/versions?ref=`). The browser cannot call OCI `tags/list`;
173
184
  * the hub holds them from ingest. */
174
185
  listVersionsForRef(ref: string): Promise<string[]>;
186
+ /** Entries of `relPath`, resolved against the ROOT of the module the manifest
187
+ * belongs to — the directory every `!include-*` / `!module-path` path is
188
+ * measured from, which for a partial is not the partial's own directory.
189
+ * Returns [] if the path doesn't exist or isn't a directory. */
190
+ listModuleEntries(relPath: string): Promise<ModuleEntry[]>;
191
+ }
192
+
193
+ export interface ModuleEntry {
194
+ name: string;
195
+ directory: boolean;
175
196
  }
176
197
 
177
198
  export interface NormalizedDiagnostic {
@@ -0,0 +1,127 @@
1
+ import { checkSchemaCompatibility } from "@telorun/analyzer";
2
+ import { builtinEngines, producedTypeOf } from "@telorun/templating";
3
+
4
+ /**
5
+ * A YAML tag an author may write on a value, and what it means to write one.
6
+ *
7
+ * The split with `@telorun/templating` is deliberate. An engine declares what a
8
+ * tag PRODUCES (`producedType()`) and where its CEL is (`expressionRegions`);
9
+ * this declares how the tag is PRESENTED to an author, which is editor knowledge
10
+ * no engine should carry. Applicability is derived from the engine's own
11
+ * declaration rather than from a list of names here — so the table says how to
12
+ * describe a tag, never which tags fit where.
13
+ *
14
+ * Shared by every editor host (VS Code completion, studio's source view and
15
+ * schema form), so the hosts cannot disagree about which tags a field takes.
16
+ */
17
+ export interface ValueTag {
18
+ /** Engine name, which is the YAML tag without its `!`. */
19
+ id: string;
20
+ /** How the tag is written. */
21
+ label: string;
22
+ /** One line on what the tag does. */
23
+ hint: string;
24
+ /** Only meaningful where the slot is EVALUATED — the tag decides what
25
+ * evaluation does with the value (`!cel` supplies the expression, `!literal`
26
+ * opts out of interpolation), so outside such a field it says nothing the
27
+ * plain value does not. An embed is the other case: it supplies a value, and
28
+ * evaluation was never involved. */
29
+ requiresEvalSlot?: boolean;
30
+ /** Set when the scalar under the tag is a module-root-relative location of
31
+ * something that ships with the module, saying what it may name. */
32
+ names?: "file" | "file-or-directory";
33
+ }
34
+
35
+ /**
36
+ * The tags an author may write, by engine name.
37
+ *
38
+ * `!ref` is absent on purpose: it names a RESOURCE rather than producing a
39
+ * value, so it belongs to a reference slot, never to a value one. `!sql` is
40
+ * absent until a host can edit it as SQL — a plain text box would be the wrong
41
+ * widget, and the hosts offer one set. An engine with no entry is simply not
42
+ * offered, which is the safe direction.
43
+ */
44
+ const AUTHORABLE: Record<string, Omit<ValueTag, "id">> = {
45
+ cel: {
46
+ label: "!cel",
47
+ hint: "A CEL expression, evaluated against this field's scope.",
48
+ requiresEvalSlot: true,
49
+ },
50
+ interpolate: {
51
+ label: "!interpolate",
52
+ hint: "Text with `${{ }}` holes, each a CEL expression; always a string.",
53
+ requiresEvalSlot: true,
54
+ },
55
+ literal: {
56
+ label: "!literal",
57
+ hint: "Opaque text. `${{ }}` inside it is not interpolated.",
58
+ requiresEvalSlot: true,
59
+ },
60
+ "include-text": {
61
+ label: "!include-text",
62
+ hint: "Contents of a file shipped with this module, as text.",
63
+ names: "file",
64
+ },
65
+ "include-bytes": {
66
+ label: "!include-bytes",
67
+ hint: "Contents of a file shipped with this module, as raw bytes.",
68
+ names: "file",
69
+ },
70
+ "module-path": {
71
+ label: "!module-path",
72
+ hint: "Location of a file or directory shipped with this module.",
73
+ names: "file-or-directory",
74
+ },
75
+ };
76
+
77
+ /** The authorable tag an engine name denotes, or undefined for a tag no host
78
+ * offers (`!ref`, `!sql`, an unknown one). */
79
+ export function valueTag(id: string): ValueTag | undefined {
80
+ const entry = AUTHORABLE[id];
81
+ return entry ? { id, ...entry } : undefined;
82
+ }
83
+
84
+ /**
85
+ * The tags offerable at one field.
86
+ *
87
+ * Two rules, both read off the engine rather than off its name:
88
+ *
89
+ * - CAN its value satisfy the slot? A tag that declares a produced type is
90
+ * offered only where that type fits. This is what puts `!include-bytes` on a
91
+ * `Telo.Bytes` slot and keeps it off a string one — and what keeps
92
+ * `!literal`, which is always text, off a boolean predicate. Checked with
93
+ * the analyzer's own comparator so the editor and `telo check` agree about
94
+ * what fits. A tag declaring no produced type (`!cel`) produces whatever the
95
+ * slot says and constrains nothing here.
96
+ * - Is it MEANINGFUL here? A tag that decides what evaluation does with the
97
+ * value needs a slot that is evaluated at all: outside one, `!cel` is a
98
+ * value the runtime never evaluates (`CEL_IN_NON_EVAL_FIELD`), and
99
+ * `!literal` suppresses an interpolation that was never going to happen.
100
+ *
101
+ * `prop` undefined is a field with no declared schema, which constrains
102
+ * nothing. `evalMode` undefined means no rule decides whether the field is
103
+ * evaluated, so the second question is not asked.
104
+ */
105
+ export function offeredValueTags(
106
+ prop: Record<string, unknown> | undefined,
107
+ evalMode: "compile" | "runtime" | null | undefined,
108
+ ): ValueTag[] {
109
+ const out: ValueTag[] = [];
110
+ for (const engine of builtinEngines) {
111
+ const tag = valueTag(engine.name);
112
+ if (!tag) continue;
113
+ const produced = producedTypeOf(engine.name);
114
+ const fitsSlot = produced && prop ? producedFits(produced, prop) : true;
115
+ const meaningful = tag.requiresEvalSlot && evalMode !== undefined ? evalMode !== null : true;
116
+ if (fitsSlot && meaningful) out.push(tag);
117
+ }
118
+ return out;
119
+ }
120
+
121
+ /** Whether a tag's produced type satisfies the slot's declared one. An
122
+ * undeclared slot accepts anything — it constrains nothing, so nothing about
123
+ * the value can contradict it. A union declares through its branches. */
124
+ function producedFits(produced: Record<string, unknown>, prop: Record<string, unknown>): boolean {
125
+ if (!prop.type && !prop["x-telo-type"] && !prop.anyOf && !prop.oneOf) return true;
126
+ return checkSchemaCompatibility(produced, prop).compatible;
127
+ }