@stepcode/codemirror 2.0.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/index.js ADDED
@@ -0,0 +1,1722 @@
1
+ import { Language, LanguageSupport, bracketMatching, defineLanguageFacet, foldGutter, foldNodeProp, indentNodeProp, indentOnInput, languageDataProp, syntaxTree } from "@codemirror/language";
2
+ import { Decoration, EditorView, GutterMarker, gutter, hoverTooltip, keymap, showTooltip } from "@codemirror/view";
3
+ import { autocompletion, snippetCompletion } from "@codemirror/autocomplete";
4
+ import { BUILTIN_KEYS, KEYWORD_KEYS, TYPE_KEYS } from "@stepcode/profiles";
5
+ import { BUILTIN_SIGNATURES, LineMap, UNKNOWN, arrayOf, childrenOf, compile, formatDiagnostic, scalar, typeToString } from "stepcode";
6
+ import { NodeProp, NodeSet, NodeType, Parser, Tree } from "@lezer/common";
7
+ import { styleTags, tags } from "@lezer/highlight";
8
+ import { EditorState, MapMode, RangeSet, RangeSetBuilder, StateEffect, StateField } from "@codemirror/state";
9
+ import { linter } from "@codemirror/lint";
10
+ //#region src/arrow.ts
11
+ const ARROW = "←";
12
+ /** The two nodes whose text is prose, where `<-` is just two characters (spec §4.3). */
13
+ const LITERAL_NODES = /* @__PURE__ */ new Set(["String", "Comment"]);
14
+ function inLiteral(state, at) {
15
+ let node = syntaxTree(state).resolveInner(at, -1);
16
+ for (;;) {
17
+ if (LITERAL_NODES.has(node.name)) return true;
18
+ const parent = node.parent;
19
+ if (parent === null) return false;
20
+ node = parent;
21
+ }
22
+ }
23
+ /**
24
+ * Spec §5.11: typing the second character of `<-` writes `←` instead, so the document keeps the
25
+ * spelling the profile prints while the keyboard keeps the one it can reach. Off for a profile
26
+ * that does not spell the arrow, or that assigns with `=`.
27
+ */
28
+ function arrowInput(profile) {
29
+ if (profile.options.assignWithEquals) return [];
30
+ if (!profile.operators.assign.includes(ARROW)) return [];
31
+ return EditorView.inputHandler.of((view, from, to, text) => {
32
+ if (text !== "-" || from !== to || from === 0) return false;
33
+ if (view.state.selection.ranges.length > 1 || view.composing) return false;
34
+ if (view.state.sliceDoc(from - 1, from) !== "<") return false;
35
+ if (inLiteral(view.state, from)) return false;
36
+ view.dispatch({
37
+ changes: {
38
+ from: from - 1,
39
+ to,
40
+ insert: ARROW
41
+ },
42
+ selection: { anchor: from },
43
+ userEvent: "input.type",
44
+ scrollIntoView: true
45
+ });
46
+ return true;
47
+ });
48
+ }
49
+ //#endregion
50
+ //#region src/nodes.ts
51
+ /** One node per AST kind, plus the two plain records the tree keeps as nodes (spec §4.2). */
52
+ const STRUCTURE_NAMES = [
53
+ "Program",
54
+ "MainBlock",
55
+ "SubprogramDecl",
56
+ "Param",
57
+ "TypeRef",
58
+ "DefineStmt",
59
+ "DimensionStmt",
60
+ "DimensionItem",
61
+ "ConstantStmt",
62
+ "AssignStmt",
63
+ "WriteStmt",
64
+ "ReadStmt",
65
+ "IfStmt",
66
+ "SwitchStmt",
67
+ "SwitchCase",
68
+ "WhileStmt",
69
+ "RepeatStmt",
70
+ "ForStmt",
71
+ "BreakStmt",
72
+ "ContinueStmt",
73
+ "ReturnStmt",
74
+ "CallStmt",
75
+ "ClearStmt",
76
+ "WaitStmt",
77
+ "WaitKeyStmt",
78
+ "ErrorStmt",
79
+ "Index",
80
+ "Call",
81
+ "BuiltinCall",
82
+ "Unary",
83
+ "Binary",
84
+ "ErrorExpr"
85
+ ];
86
+ /** The identifier roles, all leaves (spec §4.3 rule 2). */
87
+ const IDENTIFIER_NAMES = [
88
+ "Identifier",
89
+ "VariableDefinition",
90
+ "SubprogramName",
91
+ "CallName"
92
+ ];
93
+ /** Every leaf type that is not a keyword. */
94
+ const LEAF_NAMES = [
95
+ ...IDENTIFIER_NAMES,
96
+ "Number",
97
+ "String",
98
+ "Boolean",
99
+ "TypeName",
100
+ "BuiltinName",
101
+ "AssignOp",
102
+ "CompareOp",
103
+ "ArithOp",
104
+ "OpenParen",
105
+ "CloseParen",
106
+ "OpenBracket",
107
+ "CloseBracket",
108
+ "Punct",
109
+ "Comment",
110
+ "Error"
111
+ ];
112
+ /** `if` → `IfKeyword`, `writeNoNewline` → `WriteNoNewlineKeyword`. */
113
+ function keywordNodeName(key) {
114
+ return `${key.charAt(0).toUpperCase()}${key.slice(1)}Keyword`;
115
+ }
116
+ /** Opener ↔ closer, the pairs the bracket matcher and the fold rule know (spec §4.2). */
117
+ const MATCHING_PAIRS = [
118
+ ["if", "endIf"],
119
+ ["switch", "endSwitch"],
120
+ ["while", "endWhile"],
121
+ ["for", "endFor"],
122
+ ["repeat", "until"],
123
+ ["procedure", "endProcedure"],
124
+ ["function", "endFunction"],
125
+ ["program", "endProgram"]
126
+ ];
127
+ /**
128
+ * Opener ↔ closer punctuation node names (spec §4.2). The stock bracket matcher's text
129
+ * fallback only pairs characters whose tree nodes share one type, and ours are distinct types
130
+ * by design, so these get the same `closedBy`/`openedBy` treatment as the keyword pairs.
131
+ */
132
+ const PUNCT_MATCHING_PAIRS = [["OpenParen", "CloseParen"], ["OpenBracket", "CloseBracket"]];
133
+ const NODE_NAMES = [
134
+ ...STRUCTURE_NAMES,
135
+ ...LEAF_NAMES,
136
+ ...KEYWORD_KEYS.map(keywordNodeName)
137
+ ];
138
+ const ids = new Map(NODE_NAMES.map((name, id) => [name, id]));
139
+ /** The id of a node type in `nodeSet`; unknown names are a programming error. */
140
+ function nodeId(name) {
141
+ const id = ids.get(name);
142
+ if (id === void 0) throw new Error(`unknown node type: ${name}`);
143
+ return id;
144
+ }
145
+ const ERROR_NAMES = /* @__PURE__ */ new Set([
146
+ "Error",
147
+ "ErrorStmt",
148
+ "ErrorExpr"
149
+ ]);
150
+ const closers = new Map([...MATCHING_PAIRS.map(([open, close]) => [keywordNodeName(open), keywordNodeName(close)]), ...PUNCT_MATCHING_PAIRS.map(([open, close]) => [open, close])]);
151
+ const openers = new Map([...MATCHING_PAIRS.map(([open, close]) => [keywordNodeName(close), keywordNodeName(open)]), ...PUNCT_MATCHING_PAIRS.map(([open, close]) => [close, open])]);
152
+ function propsFor(name) {
153
+ const closer = closers.get(name);
154
+ if (closer !== void 0) return [[NodeProp.closedBy, [closer]]];
155
+ const opener = openers.get(name);
156
+ if (opener !== void 0) return [[NodeProp.openedBy, [opener]]];
157
+ return [];
158
+ }
159
+ const keywords = (keys) => keys.map(keywordNodeName).join(" ");
160
+ const CONTROL = [
161
+ "if",
162
+ "then",
163
+ "elseIf",
164
+ "else",
165
+ "endIf",
166
+ "switch",
167
+ "case",
168
+ "otherwise",
169
+ "endSwitch",
170
+ "while",
171
+ "do",
172
+ "endWhile",
173
+ "for",
174
+ "to",
175
+ "step",
176
+ "endFor",
177
+ "repeat",
178
+ "until",
179
+ "break",
180
+ "continue",
181
+ "return"
182
+ ];
183
+ const DEFINITION = [
184
+ "program",
185
+ "endProgram",
186
+ "define",
187
+ "as",
188
+ "constant",
189
+ "dimension",
190
+ "procedure",
191
+ "endProcedure",
192
+ "function",
193
+ "endFunction",
194
+ "byRef",
195
+ "byValue"
196
+ ];
197
+ const OPERATOR = [
198
+ "and",
199
+ "or",
200
+ "not",
201
+ "mod",
202
+ "div"
203
+ ];
204
+ const IO = [
205
+ "write",
206
+ "writeNoNewline",
207
+ "read",
208
+ "clearScreen",
209
+ "wait",
210
+ "waitKey"
211
+ ];
212
+ /** Spec §5.1, as one `styleTags` source. `true`/`false` only appear inside `Boolean` leaves. */
213
+ const highlighting = styleTags({
214
+ [keywords(CONTROL)]: tags.controlKeyword,
215
+ [keywords(DEFINITION)]: tags.definitionKeyword,
216
+ [keywords(OPERATOR)]: tags.operatorKeyword,
217
+ [keywords(IO)]: tags.keyword,
218
+ [keywords(["true", "false"])]: tags.bool,
219
+ TypeName: tags.typeName,
220
+ BuiltinName: tags.function(tags.standard(tags.variableName)),
221
+ AssignOp: tags.definitionOperator,
222
+ CompareOp: tags.compareOperator,
223
+ ArithOp: tags.arithmeticOperator,
224
+ Number: tags.number,
225
+ String: tags.string,
226
+ Boolean: tags.bool,
227
+ Comment: tags.lineComment,
228
+ Identifier: tags.variableName,
229
+ VariableDefinition: tags.definition(tags.variableName),
230
+ SubprogramName: tags.function(tags.definition(tags.variableName)),
231
+ CallName: tags.function(tags.variableName),
232
+ "OpenParen CloseParen": tags.paren,
233
+ "OpenBracket CloseBracket": tags.squareBracket,
234
+ Punct: tags.separator,
235
+ "Error ErrorStmt ErrorExpr": tags.invalid
236
+ });
237
+ /**
238
+ * The one node set. Built once at module load; `stepcodeLanguage` extends it per profile with
239
+ * the language data prop on `Program`, which changes no id or name.
240
+ */
241
+ const nodeSet = new NodeSet(NODE_NAMES.map((name, id) => NodeType.define({
242
+ id,
243
+ name,
244
+ top: name === "Program",
245
+ error: ERROR_NAMES.has(name),
246
+ props: propsFor(name)
247
+ }))).extend(highlighting);
248
+ //#endregion
249
+ //#region src/blocks.ts
250
+ const BLOCK_NAMES = [
251
+ "IfStmt",
252
+ "SwitchStmt",
253
+ "SwitchCase",
254
+ "WhileStmt",
255
+ "RepeatStmt",
256
+ "ForStmt",
257
+ "SubprogramDecl",
258
+ "MainBlock"
259
+ ];
260
+ /** The keyword leaf that closes each block, when it has one. */
261
+ const CLOSERS = {
262
+ IfStmt: ["endIf"],
263
+ SwitchStmt: ["endSwitch"],
264
+ SwitchCase: [],
265
+ WhileStmt: ["endWhile"],
266
+ RepeatStmt: ["until", "while"],
267
+ ForStmt: ["endFor"],
268
+ SubprogramDecl: ["endProcedure", "endFunction"],
269
+ MainBlock: ["endProgram"]
270
+ };
271
+ /** Lines that sit at the block's own column (spec §5.4). `SwitchStmt` handles its own. */
272
+ const DEDENT = {
273
+ IfStmt: [
274
+ "elseIf",
275
+ "else",
276
+ "endIf"
277
+ ],
278
+ SwitchStmt: ["endSwitch"],
279
+ SwitchCase: ["otherwise", "endSwitch"],
280
+ WhileStmt: ["endWhile"],
281
+ RepeatStmt: ["until"],
282
+ ForStmt: ["endFor"],
283
+ SubprogramDecl: ["endProcedure", "endFunction"],
284
+ MainBlock: ["endProgram"]
285
+ };
286
+ const ALL_DEDENT_KEYS = [...new Set(Object.values(DEDENT).flat())];
287
+ /** `valor:` on its own — a case line under `Segun` (plan deviation 4). */
288
+ const CASE_LINE = /^\s*[^:\s][^:]*:\s*$/;
289
+ /** The closer keyword leaf of a block node, or null when it is missing or the block has none. */
290
+ function closerOf(node) {
291
+ const closers = CLOSERS[node.name] ?? [];
292
+ for (const key of closers) {
293
+ const found = node.getChild(keywordNodeName(key));
294
+ if (found !== null) return found;
295
+ }
296
+ return null;
297
+ }
298
+ /** Spec §5.3. */
299
+ function foldBlock(node, state) {
300
+ const from = state.doc.lineAt(node.from).to;
301
+ const closer = closerOf(node);
302
+ const to = closer === null ? node.to : closer.from;
303
+ return from < to ? {
304
+ from,
305
+ to
306
+ } : null;
307
+ }
308
+ /**
309
+ * Does `text` start with one of `keys`, spelled per `profile`? Longest phrase first, up to the
310
+ * profile's longest keyword, so `Sino Si` beats `Sino`; a trailing colon is ignored.
311
+ */
312
+ function startsWithKeyword(profile, text, keys) {
313
+ const words = text.trim().split(/\s+/).filter((word) => word.length > 0);
314
+ for (let count = Math.min(profile.maxWords, words.length); count >= 1; count--) {
315
+ const phrase = words.slice(0, count).join(" ").replace(/:$/, "");
316
+ const entry = profile.lookup.get(profile.normalize(phrase));
317
+ if (entry?.kind === "keyword") return keys.includes(entry.key);
318
+ }
319
+ return false;
320
+ }
321
+ /** The end of the last non-blank text before `upto`, or null when there is none. */
322
+ function lastTextBefore(context, upto) {
323
+ const doc = context.state.doc;
324
+ let pos = upto;
325
+ while (pos > 0) {
326
+ const line = doc.lineAt(pos - 1);
327
+ const text = doc.sliceString(line.from, Math.min(line.to, pos)).trimEnd();
328
+ if (text.length > 0) return line.from + text.length;
329
+ pos = line.from;
330
+ }
331
+ return null;
332
+ }
333
+ /** The direct child of `block` that contains the previous non-blank line's end, if any. */
334
+ function previousChild(context, block) {
335
+ const end = lastTextBefore(context, context.lineAt(context.pos, -1).from);
336
+ if (end === null) return null;
337
+ let node = syntaxTree(context.state).resolveInner(end, -1);
338
+ while (node !== null) {
339
+ const parent = node.parent;
340
+ if (parent === null) return null;
341
+ if (parent.from === block.from && parent.name === block.name) return node;
342
+ node = parent;
343
+ }
344
+ return null;
345
+ }
346
+ function indentSwitch(context, profile) {
347
+ const text = context.textAfter;
348
+ const base = context.baseIndent;
349
+ if (startsWithKeyword(profile, text, ["endSwitch"])) return base;
350
+ if (startsWithKeyword(profile, text, ["otherwise"]) || CASE_LINE.test(text)) return base + context.unit;
351
+ const previous = previousChild(context, context.node);
352
+ if (previous === null) return base + context.unit;
353
+ if (previous.name === "SwitchCase") return context.lineIndent(previous.from) + context.unit;
354
+ const otherwise = context.node.getChild(keywordNodeName("otherwise"));
355
+ if (otherwise !== null && previous.from >= otherwise.from && previous.name !== keywordNodeName("endSwitch")) return base + 2 * context.unit;
356
+ return base + context.unit;
357
+ }
358
+ function indentCase(context, profile) {
359
+ const text = context.textAfter;
360
+ if (startsWithKeyword(profile, text, DEDENT.SwitchCase) || CASE_LINE.test(text)) return context.continue();
361
+ return context.baseIndent + context.unit;
362
+ }
363
+ const BLOCK_NAME_SET = new Set(BLOCK_NAMES);
364
+ /**
365
+ * A nested block that ends on the previous non-blank line without its closer: error recovery
366
+ * cut it short (`Segun x Hacer` with no case yet), so the line being indented is still its body.
367
+ */
368
+ function unclosedOpenerBefore(context, block) {
369
+ const previous = previousChild(context, block);
370
+ if (previous === null || !BLOCK_NAME_SET.has(previous.name)) return null;
371
+ return closerOf(previous) === null ? previous : null;
372
+ }
373
+ /**
374
+ * The indentation of a line that follows `block`'s body without being part of `block` in the tree:
375
+ * its own column for a line that closes it (or, under a case, opens the next one), one unit past
376
+ * that column otherwise.
377
+ */
378
+ function indentAfterBlock(context, profile, block) {
379
+ const column = context.lineIndent(block.from);
380
+ const name = block.name;
381
+ if (startsWithKeyword(profile, context.textAfter, DEDENT[name])) return column;
382
+ if (name === "SwitchCase" && CASE_LINE.test(context.textAfter)) return column;
383
+ return column + context.unit;
384
+ }
385
+ function indentBlock(context, profile) {
386
+ const name = context.node.name;
387
+ if (name === "SwitchStmt") return indentSwitch(context, profile);
388
+ if (name === "SwitchCase") return indentCase(context, profile);
389
+ if (startsWithKeyword(profile, context.textAfter, DEDENT[name])) return context.baseIndent;
390
+ const opener = unclosedOpenerBefore(context, context.node);
391
+ if (opener !== null) return indentAfterBlock(context, profile, opener);
392
+ return context.baseIndent + context.unit;
393
+ }
394
+ /** The innermost block still waiting for its closer at the last non-blank text before `context.pos`. */
395
+ function openBlockBefore(context) {
396
+ const end = lastTextBefore(context, context.lineAt(context.pos, -1).from);
397
+ if (end === null) return null;
398
+ let node = syntaxTree(context.state).resolveInner(end, -1);
399
+ while (node !== null) {
400
+ if (BLOCK_NAME_SET.has(node.name) && closerOf(node) === null) return node;
401
+ node = node.parent;
402
+ }
403
+ return null;
404
+ }
405
+ /**
406
+ * `Program` always covers the position, so this rule catches the line being typed under a block
407
+ * whose closer does not exist yet: the tree stops at the last text, leaving no block node over the
408
+ * new line. Indent it as the open block above it would (spec §5.4).
409
+ */
410
+ function indentProgram(context, profile) {
411
+ const block = openBlockBefore(context);
412
+ return block === null ? 0 : indentAfterBlock(context, profile, block);
413
+ }
414
+ /** The fold and indent props for every block node, bound to one profile's spellings. */
415
+ function blockProps(profile) {
416
+ const fold = {};
417
+ const indent = {};
418
+ for (const name of BLOCK_NAMES) {
419
+ fold[name] = foldBlock;
420
+ indent[name] = (context) => indentBlock(context, profile);
421
+ }
422
+ indent.Program = (context) => indentProgram(context, profile);
423
+ return [foldNodeProp.add(fold), indentNodeProp.add(indent)];
424
+ }
425
+ const escapeRegExp = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
426
+ /**
427
+ * Spec §5.4: re-indent a line once it reads as a dedent keyword (every spelling of every
428
+ * dedent key, longest first) or as a case line.
429
+ */
430
+ function indentOnInputPatterns(profile) {
431
+ const spellings = [...new Set(ALL_DEDENT_KEYS.flatMap((key) => profile.keywords[key] ?? []))].sort((a, b) => b.length - a.length).map(escapeRegExp);
432
+ const flags = profile.options.caseSensitive ? "" : "i";
433
+ const alternation = spellings.length === 0 ? "(?!)" : `(?:${spellings.join("|")})`;
434
+ return [new RegExp(`^\\s*${alternation}$`, flags), /^\s*[^:\s][^:]*:$/];
435
+ }
436
+ //#endregion
437
+ //#region src/tree.ts
438
+ const compileProp = new NodeProp({ perNode: true });
439
+ const isNode = (one) => "kind" in one;
440
+ const recordName = (one) => "values" in one ? "SwitchCase" : "DimensionItem";
441
+ const bySource = (list) => list.sort((a, b) => a.tokens[0] - b.tokens[0]);
442
+ /** `childrenOf`, except that switch cases and dimension items stay whole (spec §4.3 rule 1). */
443
+ function childrenFor(one) {
444
+ if (!isNode(one)) return "values" in one ? [...one.values, ...one.body] : [one.name, ...one.sizes];
445
+ switch (one.kind) {
446
+ case "SwitchStmt": return bySource([
447
+ one.selector,
448
+ ...one.cases,
449
+ ...one.otherwise ?? []
450
+ ]);
451
+ case "DimensionStmt": return [...one.items];
452
+ default: return childrenOf(one);
453
+ }
454
+ }
455
+ /** Spec §4.3 rule 2: the leaf name of an identifier, from the field its parent holds it in. */
456
+ function identifierRole(id, parent) {
457
+ if (parent === null) return "Identifier";
458
+ if (!isNode(parent)) return !("values" in parent) && parent.name === id ? "VariableDefinition" : "Identifier";
459
+ switch (parent.kind) {
460
+ case "MainBlock": return parent.name === id ? "SubprogramName" : "Identifier";
461
+ case "SubprogramDecl":
462
+ if (parent.name === id) return "SubprogramName";
463
+ return parent.returnName === id ? "VariableDefinition" : "Identifier";
464
+ case "Param":
465
+ case "ConstantStmt": return parent.name === id ? "VariableDefinition" : "Identifier";
466
+ case "DefineStmt": return parent.names.includes(id) ? "VariableDefinition" : "Identifier";
467
+ case "Call": return parent.callee === id ? "CallName" : "Identifier";
468
+ default: return "Identifier";
469
+ }
470
+ }
471
+ function literalLeaf(literal) {
472
+ switch (literal.type) {
473
+ case "string": return "String";
474
+ case "boolean": return "Boolean";
475
+ default: return "Number";
476
+ }
477
+ }
478
+ const COMPARE = /* @__PURE__ */ new Set([
479
+ "equal",
480
+ "notEqual",
481
+ "lt",
482
+ "le",
483
+ "gt",
484
+ "ge"
485
+ ]);
486
+ const PUNCT = /* @__PURE__ */ new Map([
487
+ ["(", "OpenParen"],
488
+ [")", "CloseParen"],
489
+ ["[", "OpenBracket"],
490
+ ["]", "CloseBracket"]
491
+ ]);
492
+ /** The leaf type of a token, or `null` for the trivia the tree drops (spec §4.2). */
493
+ function tokenLeaf(token) {
494
+ switch (token.kind) {
495
+ case "keyword": return keywordNodeName(token.value);
496
+ case "type": return "TypeName";
497
+ case "builtin": return "BuiltinName";
498
+ case "operator": {
499
+ const key = token.value;
500
+ if (key === "assign") return "AssignOp";
501
+ return COMPARE.has(key) ? "CompareOp" : "ArithOp";
502
+ }
503
+ case "identifier": return "Identifier";
504
+ case "integer":
505
+ case "real": return "Number";
506
+ case "string": return "String";
507
+ case "punct": return PUNCT.get(token.text) ?? "Punct";
508
+ case "comment": return "Comment";
509
+ case "error": return "Error";
510
+ default: return null;
511
+ }
512
+ }
513
+ /**
514
+ * Emits the postfix buffer `Tree.build` wants: every child before its parent, four numbers
515
+ * per node. Recursive over the AST; a program deep enough to matter here is not one an
516
+ * editor shows.
517
+ */
518
+ var Builder = class {
519
+ tokens;
520
+ buffer = [];
521
+ identifiers = /* @__PURE__ */ new Map();
522
+ calls = /* @__PURE__ */ new Map();
523
+ constructor(tokens) {
524
+ this.tokens = tokens;
525
+ }
526
+ leaf(name, start, end) {
527
+ this.buffer.push(nodeId(name), start, end, 4);
528
+ }
529
+ /** Leaves for the tokens `first..last` (inclusive) that no child claimed. */
530
+ tokensBetween(first, last) {
531
+ for (let index = first; index <= last; index++) {
532
+ const token = this.tokens[index];
533
+ if (token === void 0) continue;
534
+ const name = tokenLeaf(token);
535
+ if (name !== null) this.leaf(name, token.span.start, token.span.end);
536
+ }
537
+ }
538
+ /**
539
+ * The children and loose tokens of `one`, without `one` itself. `range` defaults to the
540
+ * node's own token range; the top level passes the whole stream, so a comment written
541
+ * before the program or after it still becomes a leaf under `Program`.
542
+ */
543
+ emitChildren(one, range = one.tokens) {
544
+ const [first, last] = range;
545
+ let next = first;
546
+ for (const child of childrenFor(one)) {
547
+ const [childFirst, childLast] = child.tokens;
548
+ this.tokensBetween(next, childFirst - 1);
549
+ this.emit(child, one);
550
+ next = Math.max(next, childLast + 1);
551
+ }
552
+ this.tokensBetween(next, last);
553
+ }
554
+ emit(one, parent) {
555
+ if (isNode(one)) {
556
+ if (one.kind === "Identifier") {
557
+ if (one.missing === true) return;
558
+ this.identifiers.set(one.span.start, one);
559
+ this.leaf(identifierRole(one, parent), one.span.start, one.span.end);
560
+ return;
561
+ }
562
+ if (one.kind === "Literal") {
563
+ this.leaf(literalLeaf(one), one.span.start, one.span.end);
564
+ return;
565
+ }
566
+ if (one.kind === "Call" || one.kind === "BuiltinCall") this.calls.set(one.span.start, one);
567
+ }
568
+ const start = this.buffer.length;
569
+ this.emitChildren(one);
570
+ const name = isNode(one) ? one.kind : recordName(one);
571
+ this.buffer.push(nodeId(name), one.span.start, one.span.end, this.buffer.length - start + 4);
572
+ }
573
+ };
574
+ /**
575
+ * The Lezer tree for one compile result. `set` is the node set to build with — the base set,
576
+ * or a language's extension of it; ids are the same either way.
577
+ */
578
+ function buildTree(result, set = nodeSet) {
579
+ const builder = new Builder(result.tokens);
580
+ builder.emitChildren(result.ast, [0, result.tokens.length - 1]);
581
+ const built = Tree.build({
582
+ buffer: builder.buffer,
583
+ nodeSet: set,
584
+ topID: nodeId("Program"),
585
+ length: result.source.length
586
+ });
587
+ const data = {
588
+ result,
589
+ identifiers: builder.identifiers,
590
+ calls: builder.calls
591
+ };
592
+ return new Tree(built.type, built.children, built.positions, built.length, [[compileProp, data]]);
593
+ }
594
+ //#endregion
595
+ //#region src/parser.ts
596
+ /**
597
+ * Spec §4.1: one `advance()` compiles the whole input and returns its tree. Not incremental;
598
+ * `fragments` and `ranges` are accepted and ignored, `stopAt` is recorded and ignored.
599
+ */
600
+ var StepcodeParser = class extends Parser {
601
+ profile;
602
+ set;
603
+ constructor(profile, set) {
604
+ super();
605
+ this.profile = profile;
606
+ this.set = set;
607
+ }
608
+ createParse(input, _fragments, _ranges) {
609
+ const profile = this.profile;
610
+ const set = this.set;
611
+ let parsedPos = 0;
612
+ let stoppedAt = null;
613
+ return {
614
+ get parsedPos() {
615
+ return parsedPos;
616
+ },
617
+ get stoppedAt() {
618
+ return stoppedAt;
619
+ },
620
+ stopAt(pos) {
621
+ stoppedAt = pos;
622
+ },
623
+ advance() {
624
+ const tree = buildTree(compile(input.read(0, input.length), { profile }), set);
625
+ parsedPos = input.length;
626
+ return tree;
627
+ }
628
+ };
629
+ }
630
+ };
631
+ /** The language data for a profile: comment tokens only. */
632
+ function languageData(profile) {
633
+ return { commentTokens: { line: profile.operators.comment[0] ?? "//" } };
634
+ }
635
+ const languages = /* @__PURE__ */ new WeakMap();
636
+ /**
637
+ * One `Language` per profile object, cached: `stepcodeCompletion` registers through its data
638
+ * facet, so every extension built for a profile must see the same instance.
639
+ *
640
+ * The cache keys on object identity (a `WeakMap`), so a profile must not be mutated after it is
641
+ * first passed here — a later mutation would silently apply to the cached `Language` too.
642
+ */
643
+ function stepcodeLanguage(profile) {
644
+ const cached = languages.get(profile);
645
+ if (cached !== void 0) return cached;
646
+ const data = defineLanguageFacet(languageData(profile));
647
+ const set = nodeSet.extend(languageDataProp.add({ Program: data }), ...blockProps(profile));
648
+ const rules = indentOnInputPatterns(profile).map((pattern) => data.of({ indentOnInput: pattern }));
649
+ const language = new Language(data, new StepcodeParser(profile, set), rules, "stepcode");
650
+ languages.set(profile, language);
651
+ return language;
652
+ }
653
+ /** The data on the current tree, or `null` before a parse has produced one. */
654
+ function treeDataAt(state) {
655
+ return syntaxTree(state).prop(compileProp) ?? null;
656
+ }
657
+ function compileResultAt(state) {
658
+ return treeDataAt(state)?.result ?? null;
659
+ }
660
+ //#endregion
661
+ //#region src/snippets.ts
662
+ const CURSOR = "${}";
663
+ const field = (name) => `\${${name}}`;
664
+ /**
665
+ * The templates, spelled per profile. Body lines start with a tab, which the snippet library
666
+ * turns into one indent unit relative to the line the snippet lands on.
667
+ */
668
+ function blockTemplates(profile, strings) {
669
+ const kw = (key) => profile.keywords[key]?.[0] ?? key;
670
+ const assign = profile.options.assignWithEquals ? "=" : profile.operators.assign[0] ?? "<-";
671
+ const p = strings.placeholders;
672
+ const lines = (...parts) => parts.join("\n");
673
+ return /* @__PURE__ */ new Map([
674
+ ["if", lines(`${kw("if")} ${field(p.condition)} ${kw("then")}`, `\t${CURSOR}`, kw("endIf"))],
675
+ ["while", lines(`${kw("while")} ${field(p.condition)} ${kw("do")}`, `\t${CURSOR}`, kw("endWhile"))],
676
+ ["for", lines(`${kw("for")} ${field(p.counter)} ${assign} ${field(p.start)} ${kw("to")} ${field(p.limit)} ${kw("do")}`, `\t${CURSOR}`, kw("endFor"))],
677
+ ["repeat", lines(kw("repeat"), `\t${CURSOR}`, `${kw("until")} ${field(p.condition)}`)],
678
+ ["switch", lines(`${kw("switch")} ${field(p.value)} ${kw("do")}`, `\t${field(p.case)}:`, `\t\t${CURSOR}`, `\t${kw("otherwise")}:`, " ", kw("endSwitch"))],
679
+ ["function", lines(`${kw("function")} ${field(p.result)} ${assign} ${field(p.name)}(${field(p.parameters)})`, `\t${CURSOR}`, kw("endFunction"))],
680
+ ["procedure", lines(`${kw("procedure")} ${field(p.name)}(${field(p.parameters)})`, `\t${CURSOR}`, kw("endProcedure"))],
681
+ ["program", lines(`${kw("program")} ${field(p.name)}`, `\t${CURSOR}`, kw("endProgram"))]
682
+ ]);
683
+ }
684
+ /**
685
+ * The one-line statements, spelled per profile. The trailing `;` is written only where the
686
+ * profile requires terminators, so a PSeInt-style program never gains one it would reject.
687
+ */
688
+ function statementTemplates(profile, strings) {
689
+ const kw = (key) => profile.keywords[key]?.[0] ?? key;
690
+ const p = strings.placeholders;
691
+ const end = profile.options.requireSemicolons ? ";" : "";
692
+ const stmt = (text) => `${text}${end}${CURSOR}`;
693
+ return /* @__PURE__ */ new Map([
694
+ ["define", stmt(`${kw("define")} ${field(p.variable)} ${kw("as")} ${field(p.type)}`)],
695
+ ["dimension", stmt(`${kw("dimension")} ${field(p.variable)}[${field(p.size)}]`)],
696
+ ["write", stmt(`${kw("write")} ${field(p.message)}`)],
697
+ ["writeNoNewline", stmt(`${kw("writeNoNewline")} ${field(p.message)}`)],
698
+ ["read", stmt(`${kw("read")} ${field(p.variable)}`)],
699
+ ["return", stmt(`${kw("return")} ${field(p.value)}`)],
700
+ ["break", stmt(kw("break"))],
701
+ ["continue", stmt(kw("continue"))],
702
+ ["else", `${kw("else")}\n\t${CURSOR}`],
703
+ ["elseIf", `${kw("elseIf")} ${field(p.condition)} ${kw("then")}\n\t${CURSOR}`]
704
+ ]);
705
+ }
706
+ /** One keyword completion per opener, applying its template. */
707
+ function blockSnippets(profile, strings) {
708
+ const out = /* @__PURE__ */ new Map();
709
+ for (const [key, template] of blockTemplates(profile, strings)) {
710
+ const label = profile.keywords[key]?.[0];
711
+ if (label === void 0 || label.length === 0) continue;
712
+ out.set(key, snippetCompletion(template, {
713
+ label,
714
+ type: "keyword",
715
+ info: strings.descriptions.keywords[key],
716
+ boost: 0
717
+ }));
718
+ }
719
+ return out;
720
+ }
721
+ /** Every keyword whose completion applies a template: the block openers and the statements. */
722
+ function keywordSnippets(profile, strings) {
723
+ const out = new Map(blockSnippets(profile, strings));
724
+ for (const [key, template] of statementTemplates(profile, strings)) {
725
+ const label = profile.keywords[key]?.[0];
726
+ if (label === void 0 || label.length === 0) continue;
727
+ out.set(key, snippetCompletion(template, {
728
+ label,
729
+ type: "keyword",
730
+ info: strings.descriptions.keywords[key],
731
+ boost: 0
732
+ }));
733
+ }
734
+ return out;
735
+ }
736
+ //#endregion
737
+ //#region src/strings.ts
738
+ const es = {
739
+ kinds: {
740
+ variable: "variable",
741
+ parameter: "parámetro",
742
+ result: "resultado",
743
+ constant: "constante",
744
+ counter: "contador",
745
+ subprogram: "subprograma"
746
+ },
747
+ procedure: "procedimiento",
748
+ function: "función",
749
+ byReference: "por referencia",
750
+ declaredAt: (line) => `declarada en la línea ${line}`,
751
+ replaceWith: (name) => `Cambiar a «${name}»`,
752
+ operandClass: {
753
+ numeric: "número",
754
+ text: "texto",
755
+ boolean: "lógico",
756
+ integer: "entero",
757
+ scalar: "valor"
758
+ },
759
+ same: "igual al argumento",
760
+ placeholders: {
761
+ condition: "condicion",
762
+ value: "valor",
763
+ name: "nombre",
764
+ parameters: "parametros",
765
+ result: "resultado",
766
+ counter: "contador",
767
+ start: "inicio",
768
+ limit: "limite",
769
+ case: "caso",
770
+ variable: "variable",
771
+ type: "tipo",
772
+ message: "mensaje",
773
+ size: "tamano"
774
+ },
775
+ descriptions: {
776
+ keywords: {
777
+ program: "Marca donde empieza el programa principal.",
778
+ endProgram: "Marca donde termina el programa principal.",
779
+ define: "Crea una variable nueva y dice de qué tipo es.",
780
+ as: "Une el nombre de la variable con su tipo.",
781
+ constant: "Crea un valor con nombre que nunca cambia.",
782
+ dimension: "Crea un arreglo con la cantidad de casillas que indiques.",
783
+ if: "Ejecuta unas instrucciones solo si la condición se cumple.",
784
+ then: "Empieza las instrucciones que se ejecutan cuando la condición se cumple.",
785
+ elseIf: "Prueba otra condición cuando la anterior no se cumplió.",
786
+ else: "Ejecuta estas instrucciones cuando ninguna condición se cumplió.",
787
+ endIf: "Cierra la instrucción condicional.",
788
+ switch: "Elige un camino comparando un valor con varios casos.",
789
+ case: "Empieza el camino que corresponde a un valor concreto.",
790
+ otherwise: "Empieza el camino que se toma cuando ningún caso coincide.",
791
+ endSwitch: "Cierra la selección por casos.",
792
+ while: "Repite instrucciones mientras la condición siga cumpliéndose.",
793
+ do: "Empieza el cuerpo que se repite.",
794
+ endWhile: "Cierra el ciclo que repite mientras se cumple una condición.",
795
+ for: "Repite instrucciones contando desde un valor hasta otro.",
796
+ to: "Indica el valor hasta el que cuenta el ciclo.",
797
+ step: "Indica de cuánto en cuánto avanza el contador.",
798
+ endFor: "Cierra el ciclo que cuenta.",
799
+ repeat: "Repite instrucciones al menos una vez y prueba la condición al final.",
800
+ until: "Indica la condición que detiene el ciclo.",
801
+ break: "Sale del ciclo de inmediato.",
802
+ continue: "Salta al siguiente turno del ciclo.",
803
+ procedure: "Define un grupo de instrucciones con nombre que no devuelve un valor.",
804
+ endProcedure: "Cierra la definición del subproceso.",
805
+ function: "Define un grupo de instrucciones con nombre que devuelve un valor.",
806
+ endFunction: "Cierra la definición de la función.",
807
+ return: "Termina el subprograma y devuelve un valor.",
808
+ byRef: "Hace que el parámetro comparta la variable de quien llama.",
809
+ byValue: "Hace que el parámetro reciba una copia del valor.",
810
+ write: "Muestra un valor en la consola.",
811
+ writeNoNewline: "Muestra un valor en la consola sin pasar a la línea siguiente.",
812
+ read: "Lee un valor escrito por el usuario y lo guarda en la variable.",
813
+ clearScreen: "Borra todo lo que hay escrito en la consola.",
814
+ wait: "Detiene el programa durante el tiempo indicado.",
815
+ waitKey: "Detiene el programa hasta que el usuario presione una tecla.",
816
+ and: "Es verdadero solo si las dos condiciones son verdaderas.",
817
+ or: "Es verdadero si al menos una de las dos condiciones es verdadera.",
818
+ not: "Invierte una condición: lo verdadero pasa a falso.",
819
+ mod: "Da el residuo de una división entera.",
820
+ div: "Da el cociente entero de una división.",
821
+ true: "El valor lógico verdadero.",
822
+ false: "El valor lógico falso."
823
+ },
824
+ types: {
825
+ integer: "Números sin decimales.",
826
+ real: "Números con decimales.",
827
+ string: "Texto: una serie de caracteres entre comillas.",
828
+ char: "Un solo carácter.",
829
+ boolean: "Solo dos valores posibles: verdadero o falso."
830
+ },
831
+ builtins: {
832
+ abs: "Da el valor de un número sin su signo.",
833
+ sqrt: "Da la raíz cuadrada de un número.",
834
+ ln: "Da el logaritmo natural de un número.",
835
+ exp: "Eleva el número e a la potencia indicada.",
836
+ sin: "Da el seno de un ángulo medido en radianes.",
837
+ cos: "Da el coseno de un ángulo medido en radianes.",
838
+ tan: "Da la tangente de un ángulo medido en radianes.",
839
+ asin: "Da el ángulo en radianes cuyo seno es el valor dado.",
840
+ acos: "Da el ángulo en radianes cuyo coseno es el valor dado.",
841
+ atan: "Da el ángulo en radianes cuya tangente es el valor dado.",
842
+ trunc: "Quita los decimales de un número.",
843
+ round: "Redondea un número al entero más cercano.",
844
+ random: "Da un número al azar entre 0 y 1.",
845
+ randomBetween: "Da un número entero al azar dentro del rango indicado.",
846
+ pi: "El valor de pi.",
847
+ length: "Da cuántos caracteres tiene un texto.",
848
+ upper: "Convierte un texto a mayúsculas.",
849
+ lower: "Convierte un texto a minúsculas.",
850
+ substring: "Da el trozo de texto que hay entre dos posiciones.",
851
+ concat: "Une dos textos en uno solo.",
852
+ toNumber: "Convierte un texto en número.",
853
+ toText: "Convierte un número en texto."
854
+ }
855
+ }
856
+ };
857
+ const en = {
858
+ kinds: {
859
+ variable: "variable",
860
+ parameter: "parameter",
861
+ result: "result",
862
+ constant: "constant",
863
+ counter: "counter",
864
+ subprogram: "subprogram"
865
+ },
866
+ procedure: "procedure",
867
+ function: "function",
868
+ byReference: "by reference",
869
+ declaredAt: (line) => `declared on line ${line}`,
870
+ replaceWith: (name) => `Replace with "${name}"`,
871
+ operandClass: {
872
+ numeric: "number",
873
+ text: "text",
874
+ boolean: "boolean",
875
+ integer: "integer",
876
+ scalar: "value"
877
+ },
878
+ same: "same as the argument",
879
+ placeholders: {
880
+ condition: "condition",
881
+ value: "value",
882
+ name: "name",
883
+ parameters: "parameters",
884
+ result: "result",
885
+ counter: "counter",
886
+ start: "start",
887
+ limit: "limit",
888
+ case: "case",
889
+ variable: "variable",
890
+ type: "type",
891
+ message: "message",
892
+ size: "size"
893
+ },
894
+ descriptions: {
895
+ keywords: {
896
+ program: "Marks where the main program starts.",
897
+ endProgram: "Marks where the main program ends.",
898
+ define: "Creates a new variable and says what type it holds.",
899
+ as: "Joins a variable name to its type.",
900
+ constant: "Creates a named value that never changes.",
901
+ dimension: "Creates an array with as many slots as you ask for.",
902
+ if: "Runs some instructions only when the condition holds.",
903
+ then: "Starts the instructions that run when the condition holds.",
904
+ elseIf: "Tries another condition when the previous one did not hold.",
905
+ else: "Runs these instructions when no condition held.",
906
+ endIf: "Closes the conditional instruction.",
907
+ switch: "Picks one path by comparing a value against several cases.",
908
+ case: "Starts the path for one particular value.",
909
+ otherwise: "Starts the path taken when no case matches.",
910
+ endSwitch: "Closes the selection by cases.",
911
+ while: "Repeats instructions while the condition keeps holding.",
912
+ do: "Starts the body that repeats.",
913
+ endWhile: "Closes the loop that repeats while a condition holds.",
914
+ for: "Repeats instructions counting from one value up to another.",
915
+ to: "Gives the value the loop counts up to.",
916
+ step: "Gives how much the counter advances each turn.",
917
+ endFor: "Closes the counting loop.",
918
+ repeat: "Repeats instructions at least once and tests the condition at the end.",
919
+ until: "Gives the condition that stops the loop.",
920
+ break: "Leaves the loop right away.",
921
+ continue: "Jumps to the next turn of the loop.",
922
+ procedure: "Defines a named group of instructions that returns no value.",
923
+ endProcedure: "Closes the procedure definition.",
924
+ function: "Defines a named group of instructions that returns a value.",
925
+ endFunction: "Closes the function definition.",
926
+ return: "Ends the subprogram and hands a value back.",
927
+ byRef: "Makes the parameter share the caller variable.",
928
+ byValue: "Makes the parameter receive a copy of the value.",
929
+ write: "Shows a value in the console.",
930
+ writeNoNewline: "Shows a value in the console without moving to the next line.",
931
+ read: "Reads a value typed by the user and stores it in the variable.",
932
+ clearScreen: "Erases everything written in the console.",
933
+ wait: "Pauses the program for the time given.",
934
+ waitKey: "Pauses the program until the user presses a key.",
935
+ and: "True only when both conditions are true.",
936
+ or: "True when at least one of the two conditions is true.",
937
+ not: "Flips a condition: what was true becomes false.",
938
+ mod: "Gives the remainder of an integer division.",
939
+ div: "Gives the whole-number quotient of a division.",
940
+ true: "The boolean value true.",
941
+ false: "The boolean value false."
942
+ },
943
+ types: {
944
+ integer: "Numbers with no decimals.",
945
+ real: "Numbers with decimals.",
946
+ string: "Text: a run of characters between quotes.",
947
+ char: "A single character.",
948
+ boolean: "Only two possible values: true or false."
949
+ },
950
+ builtins: {
951
+ abs: "Gives a number without its sign.",
952
+ sqrt: "Gives the square root of a number.",
953
+ ln: "Gives the natural logarithm of a number.",
954
+ exp: "Raises the number e to the given power.",
955
+ sin: "Gives the sine of an angle measured in radians.",
956
+ cos: "Gives the cosine of an angle measured in radians.",
957
+ tan: "Gives the tangent of an angle measured in radians.",
958
+ asin: "Gives the angle in radians whose sine is the given value.",
959
+ acos: "Gives the angle in radians whose cosine is the given value.",
960
+ atan: "Gives the angle in radians whose tangent is the given value.",
961
+ trunc: "Drops the decimals of a number.",
962
+ round: "Rounds a number to the nearest whole number.",
963
+ random: "Gives a random number between 0 and 1.",
964
+ randomBetween: "Gives a random whole number inside the given range.",
965
+ pi: "The value of pi.",
966
+ length: "Gives how many characters a text has.",
967
+ upper: "Turns a text into upper case.",
968
+ lower: "Turns a text into lower case.",
969
+ substring: "Gives the piece of text between two positions.",
970
+ concat: "Joins two texts into one.",
971
+ toNumber: "Turns a text into a number.",
972
+ toText: "Turns a number into text."
973
+ }
974
+ }
975
+ };
976
+ const TABLES = {
977
+ es,
978
+ en
979
+ };
980
+ /** The table for a BCP-47 tag: exact, then primary subtag (`es-MX` → `es`), then `en`. */
981
+ function stringsFor(locale) {
982
+ const exact = TABLES[locale];
983
+ if (exact !== void 0) return exact;
984
+ const primary = locale.split("-")[0] ?? "";
985
+ return TABLES[primary] ?? en;
986
+ }
987
+ //#endregion
988
+ //#region src/symbols.ts
989
+ const IDENTIFIERS = new Set(IDENTIFIER_NAMES);
990
+ /**
991
+ * The identifier leaf ending at `pos` (side -1), starting at it (side 1), or either (0, the
992
+ * leaf ending there first) — a cursor touches a word from both sides.
993
+ */
994
+ function identifierLeafAt(state, pos, side = 0) {
995
+ const tree = syntaxTree(state);
996
+ const sides = side === 0 ? [-1, 1] : [side];
997
+ for (const one of sides) {
998
+ const node = tree.resolveInner(pos, one);
999
+ if (IDENTIFIERS.has(node.name)) return node;
1000
+ }
1001
+ return null;
1002
+ }
1003
+ /** The leaf at `pos` and the checker symbol it resolved to, or null. */
1004
+ function symbolAt(state, pos, side = 0) {
1005
+ const leaf = identifierLeafAt(state, pos, side);
1006
+ const data = treeDataAt(state);
1007
+ if (leaf === null || data === null) return null;
1008
+ const identifier = data.identifiers.get(leaf.from);
1009
+ if (identifier === void 0) return null;
1010
+ const symbol = data.result.symbols.get(identifier);
1011
+ return symbol === void 0 ? null : {
1012
+ leaf,
1013
+ symbol
1014
+ };
1015
+ }
1016
+ /**
1017
+ * The innermost body scope whose owner contains `pos`, else the program scope. Bodies do nest:
1018
+ * a subprogram written inside another one (E2015) keeps its place in the source, so its span
1019
+ * lies inside the enclosing body's. `scopes` is build order, not nesting order, so the
1020
+ * narrowest containing owner wins rather than the first one listed.
1021
+ */
1022
+ function scopeAt(result, pos) {
1023
+ const program = result.scopes[0];
1024
+ if (program === void 0) throw new Error("a compile result always has a program scope");
1025
+ let innermost = program;
1026
+ let width = Number.POSITIVE_INFINITY;
1027
+ for (const scope of result.scopes) {
1028
+ if (scope.kind !== "body") continue;
1029
+ const { span } = scope.owner;
1030
+ if (span.start > pos || pos > span.end || span.end - span.start >= width) continue;
1031
+ innermost = scope;
1032
+ width = span.end - span.start;
1033
+ }
1034
+ return innermost;
1035
+ }
1036
+ /**
1037
+ * Spec §5.6: the symbols usable at `pos` — the scope chain from the innermost, a name once,
1038
+ * declarations after the cursor excluded except subprograms, recovery symbols never.
1039
+ */
1040
+ function visibleSymbols(result, pos) {
1041
+ const seen = /* @__PURE__ */ new Set();
1042
+ const out = [];
1043
+ for (let scope = scopeAt(result, pos); scope !== null; scope = scope.parent) for (const symbol of scope.order) {
1044
+ if (seen.has(symbol.name) || symbol.recovered === true) continue;
1045
+ if (symbol.kind !== "subprogram" && symbol.declaredAt.span.start >= pos) continue;
1046
+ seen.add(symbol.name);
1047
+ out.push(symbol);
1048
+ }
1049
+ return out;
1050
+ }
1051
+ /** The name as the declaration wrote it; a result variable's from the header. */
1052
+ function symbolLabel(symbol) {
1053
+ const at = symbol.declaredAt;
1054
+ if (at.kind === "Identifier") return at.text;
1055
+ if (at.kind === "SubprogramDecl" && at.returnName !== void 0) return at.returnName.text;
1056
+ return symbol.name;
1057
+ }
1058
+ /** The builtin a spelling names under `profile`, or null. */
1059
+ function builtinKeyAt(profile, text) {
1060
+ const entry = profile.lookup.get(profile.normalize(text));
1061
+ return entry?.kind === "builtin" ? entry.key : null;
1062
+ }
1063
+ /** `Name(p1, p2) : result`, spec §5.6, with the parameter at `activeIndex` flagged. */
1064
+ function builtinSignatureParts(key, profile, strings, activeIndex = -1) {
1065
+ const signature = BUILTIN_SIGNATURES[key];
1066
+ const parts = [{
1067
+ text: `${profile.builtins[key]?.[0] ?? key}(`,
1068
+ active: false
1069
+ }];
1070
+ signature.params.forEach((operand, index) => {
1071
+ if (index > 0) parts.push({
1072
+ text: ", ",
1073
+ active: false
1074
+ });
1075
+ parts.push({
1076
+ text: strings.operandClass[operand],
1077
+ active: index === activeIndex
1078
+ });
1079
+ });
1080
+ const result = signature.result === "same" ? strings.same : typeToString(signature.result, profile);
1081
+ parts.push({
1082
+ text: `) : ${result}`,
1083
+ active: false
1084
+ });
1085
+ return parts;
1086
+ }
1087
+ const signatureText = (parts) => parts.map((part) => part.text).join("");
1088
+ //#endregion
1089
+ //#region src/completion.ts
1090
+ const WORD = /[\p{L}_][\p{L}\p{N}_]*$/u;
1091
+ const VALID = /^[\p{L}_][\p{L}\p{N}_]*$/u;
1092
+ const BOOST = {
1093
+ symbol: 3,
1094
+ builtin: 2,
1095
+ type: 1,
1096
+ keyword: 0
1097
+ };
1098
+ /** `name(<cursor>)` for a callable with parameters, `name()<cursor>` without. */
1099
+ function callCompletion(label, hasParams, completion) {
1100
+ return snippetCompletion(hasParams ? `${label}(\${})` : `${label}()\${}`, completion);
1101
+ }
1102
+ function symbolCompletions(result, pos, profile, strings) {
1103
+ return visibleSymbols(result, pos).map((symbol) => {
1104
+ const label = symbolLabel(symbol);
1105
+ if (symbol.kind === "subprogram") {
1106
+ const decl = symbol.decl;
1107
+ return callCompletion(label, decl !== void 0 && decl.params.length > 0, {
1108
+ label,
1109
+ type: "function",
1110
+ detail: decl?.form === "function" ? strings.function : strings.procedure,
1111
+ boost: BOOST.symbol
1112
+ });
1113
+ }
1114
+ return {
1115
+ label,
1116
+ type: symbol.kind === "constant" ? "constant" : "variable",
1117
+ detail: typeToString(symbol.type, profile),
1118
+ boost: BOOST.symbol
1119
+ };
1120
+ });
1121
+ }
1122
+ function builtinCompletions(profile, strings) {
1123
+ const out = [];
1124
+ for (const key of BUILTIN_KEYS) {
1125
+ const label = profile.builtins[key]?.[0];
1126
+ if (label === void 0) continue;
1127
+ const detail = signatureText(builtinSignatureParts(key, profile, strings)).slice(label.length);
1128
+ out.push(callCompletion(label, BUILTIN_SIGNATURES[key].params.length > 0, {
1129
+ label,
1130
+ type: "function",
1131
+ detail,
1132
+ info: strings.descriptions.builtins[key],
1133
+ boost: BOOST.builtin
1134
+ }));
1135
+ }
1136
+ return out;
1137
+ }
1138
+ function typeCompletions(profile, strings) {
1139
+ const out = [];
1140
+ for (const key of TYPE_KEYS) {
1141
+ const label = profile.types[key]?.[0];
1142
+ if (label !== void 0) out.push({
1143
+ label,
1144
+ type: "type",
1145
+ info: strings.descriptions.types[key],
1146
+ boost: BOOST.type
1147
+ });
1148
+ }
1149
+ return out;
1150
+ }
1151
+ /** Every keyword with a spelling; the block openers apply their snippet (spec §5.7). */
1152
+ function keywordCompletions(profile, strings) {
1153
+ const snippets = keywordSnippets(profile, strings);
1154
+ const out = [];
1155
+ for (const key of KEYWORD_KEYS) {
1156
+ const label = profile.keywords[key]?.[0];
1157
+ if (label === void 0 || label.length === 0) continue;
1158
+ const snippet = snippets.get(key);
1159
+ out.push(snippet ?? {
1160
+ label,
1161
+ type: "keyword",
1162
+ info: strings.descriptions.keywords[key],
1163
+ boost: BOOST.keyword
1164
+ });
1165
+ }
1166
+ return out;
1167
+ }
1168
+ /** Spec §5.6. */
1169
+ function completionSourceFor(options) {
1170
+ const { profile } = options;
1171
+ const strings = stringsFor(options.locale);
1172
+ const fixed = [
1173
+ ...builtinCompletions(profile, strings),
1174
+ ...typeCompletions(profile, strings),
1175
+ ...keywordCompletions(profile, strings)
1176
+ ];
1177
+ return (context) => {
1178
+ const word = context.matchBefore(WORD);
1179
+ if (word === null && !context.explicit) return null;
1180
+ const node = syntaxTree(context.state).resolveInner(context.pos, -1);
1181
+ if (node.name === "Comment" || node.name === "String") return null;
1182
+ const data = treeDataAt(context.state);
1183
+ const symbols = data === null ? [] : symbolCompletions(data.result, context.pos, profile, strings);
1184
+ return {
1185
+ from: word?.from ?? context.pos,
1186
+ options: [...symbols, ...fixed],
1187
+ validFor: VALID
1188
+ };
1189
+ };
1190
+ }
1191
+ /** The source, registered through the language's data so `autocompletion()` picks it up. */
1192
+ function stepcodeCompletion(options) {
1193
+ return stepcodeLanguage(options.profile).data.of({ autocomplete: completionSourceFor(options) });
1194
+ }
1195
+ //#endregion
1196
+ //#region src/theme.ts
1197
+ /** Spec §8: class hooks with a minimal look; hosts restyle by class. */
1198
+ const stepcodeBaseTheme = EditorView.baseTheme({
1199
+ ".cm-gutter.cm-stepcode-breakpoints": {
1200
+ minWidth: "1.4em",
1201
+ cursor: "pointer"
1202
+ },
1203
+ ".cm-stepcode-breakpoints .cm-gutterElement": {
1204
+ display: "flex",
1205
+ alignItems: "center",
1206
+ justifyContent: "center",
1207
+ gap: "0.1em"
1208
+ },
1209
+ ".cm-stepcode-breakpoint, .cm-stepcode-breakpoint-spacer": {
1210
+ width: "0.7em",
1211
+ height: "0.7em",
1212
+ borderRadius: "50%"
1213
+ },
1214
+ ".cm-stepcode-breakpoint": { backgroundColor: "#d33" },
1215
+ ".cm-stepcode-current-line-marker": {
1216
+ width: "0",
1217
+ height: "0",
1218
+ borderTop: "0.4em solid transparent",
1219
+ borderBottom: "0.4em solid transparent",
1220
+ borderLeft: "0.6em solid #d9a400"
1221
+ },
1222
+ "&light .cm-stepcode-current-line": { backgroundColor: "rgba(255, 220, 0, 0.25)" },
1223
+ "&dark .cm-stepcode-current-line": { backgroundColor: "rgba(255, 220, 0, 0.15)" },
1224
+ ".cm-tooltip .cm-stepcode-hover, .cm-tooltip .cm-stepcode-signature": {
1225
+ padding: "0.3em 0.5em",
1226
+ fontFamily: "monospace"
1227
+ },
1228
+ ".cm-stepcode-signature-active": { fontWeight: "bold" },
1229
+ "&light .cm-matchingBracket, &dark .cm-matchingBracket": {
1230
+ backgroundColor: "transparent",
1231
+ outline: "1px solid #4a8"
1232
+ },
1233
+ "&light .cm-nonmatchingBracket, &dark .cm-nonmatchingBracket": {
1234
+ backgroundColor: "transparent",
1235
+ outline: "1px solid #c44"
1236
+ }
1237
+ });
1238
+ //#endregion
1239
+ //#region src/debug.ts
1240
+ const toggleBreakpoint = StateEffect.define();
1241
+ const setBreakpoints = StateEffect.define();
1242
+ const setCurrentLine = StateEffect.define();
1243
+ var BreakpointMarker = class BreakpointMarker extends GutterMarker {
1244
+ toDOM() {
1245
+ const dom = document.createElement("div");
1246
+ dom.className = "cm-stepcode-breakpoint";
1247
+ return dom;
1248
+ }
1249
+ eq(other) {
1250
+ return other instanceof BreakpointMarker;
1251
+ }
1252
+ };
1253
+ var CurrentLineMarker = class CurrentLineMarker extends GutterMarker {
1254
+ toDOM() {
1255
+ const dom = document.createElement("div");
1256
+ dom.className = "cm-stepcode-current-line-marker";
1257
+ return dom;
1258
+ }
1259
+ eq(other) {
1260
+ return other instanceof CurrentLineMarker;
1261
+ }
1262
+ };
1263
+ /**
1264
+ * Sizes the gutter for a breakpoint dot without rendering one: the gutter keeps this marker's
1265
+ * DOM permanently mounted (hidden) for width measurement, so it must not share the
1266
+ * `.cm-stepcode-breakpoint` class or it would inflate any count of real markers.
1267
+ */
1268
+ var SpacerMarker = class SpacerMarker extends GutterMarker {
1269
+ toDOM() {
1270
+ const dom = document.createElement("div");
1271
+ dom.className = "cm-stepcode-breakpoint-spacer";
1272
+ return dom;
1273
+ }
1274
+ eq(other) {
1275
+ return other instanceof SpacerMarker;
1276
+ }
1277
+ };
1278
+ const breakpointMarker = new BreakpointMarker();
1279
+ const currentLineMarker = new CurrentLineMarker();
1280
+ const spacerMarker = new SpacerMarker();
1281
+ /**
1282
+ * Where the start of the line at `lineFrom` lands after `changes`, or null when the line is
1283
+ * gone: its whole content was deleted, or — for an empty line — its line break was.
1284
+ */
1285
+ function mapLineStart(changes, oldDoc, lineFrom) {
1286
+ const line = oldDoc.lineAt(lineFrom);
1287
+ if (line.length === 0) return changes.mapPos(line.from, -1, MapMode.TrackAfter);
1288
+ const from = changes.mapPos(line.from, -1);
1289
+ return from === changes.mapPos(line.to, 1) ? null : from;
1290
+ }
1291
+ function markersAt(positions) {
1292
+ const builder = new RangeSetBuilder();
1293
+ let last = -1;
1294
+ for (const pos of [...positions].sort((a, b) => a - b)) {
1295
+ if (pos === last) continue;
1296
+ builder.add(pos, pos, breakpointMarker);
1297
+ last = pos;
1298
+ }
1299
+ return builder.finish();
1300
+ }
1301
+ function positionsOf(set) {
1302
+ const out = [];
1303
+ for (const cursor = set.iter(); cursor.value !== null; cursor.next()) out.push(cursor.from);
1304
+ return out;
1305
+ }
1306
+ function lineStart(state, line) {
1307
+ return line >= 1 && line <= state.doc.lines ? state.doc.line(line).from : null;
1308
+ }
1309
+ function remap(set, tr) {
1310
+ const positions = [];
1311
+ for (const pos of positionsOf(set)) {
1312
+ const mapped = mapLineStart(tr.changes, tr.startState.doc, pos);
1313
+ if (mapped !== null) positions.push(tr.state.doc.lineAt(mapped).from);
1314
+ }
1315
+ return markersAt(positions);
1316
+ }
1317
+ const breakpointField = StateField.define({
1318
+ create: () => RangeSet.empty,
1319
+ update(value, tr) {
1320
+ let set = tr.docChanged ? remap(value, tr) : value;
1321
+ for (const effect of tr.effects) if (effect.is(toggleBreakpoint)) {
1322
+ const from = lineStart(tr.state, effect.value.line);
1323
+ if (from === null) continue;
1324
+ const positions = positionsOf(set);
1325
+ set = markersAt(positions.includes(from) ? positions.filter((pos) => pos !== from) : [...positions, from]);
1326
+ } else if (effect.is(setBreakpoints)) set = markersAt(effect.value.map((line) => lineStart(tr.state, line)).filter((pos) => pos !== null));
1327
+ return set;
1328
+ }
1329
+ });
1330
+ const currentLineDecoration = Decoration.line({ class: "cm-stepcode-current-line" });
1331
+ /** The start offset of the current line, or null. */
1332
+ const currentLineField = StateField.define({
1333
+ create: () => null,
1334
+ update(value, tr) {
1335
+ let next = value;
1336
+ if (next !== null && tr.docChanged) {
1337
+ const mapped = mapLineStart(tr.changes, tr.startState.doc, next);
1338
+ next = mapped === null ? null : tr.state.doc.lineAt(mapped).from;
1339
+ }
1340
+ for (const effect of tr.effects) if (effect.is(setCurrentLine)) next = effect.value === null ? null : lineStart(tr.state, effect.value);
1341
+ return next;
1342
+ },
1343
+ provide: (field) => EditorView.decorations.from(field, (value) => value === null ? Decoration.none : Decoration.set(currentLineDecoration.range(value)))
1344
+ });
1345
+ /** Spec §6.2: setting a line also scrolls it into view. */
1346
+ const scrollToCurrentLine = EditorState.transactionExtender.of((tr) => {
1347
+ for (const effect of tr.effects) {
1348
+ if (!effect.is(setCurrentLine) || effect.value === null) continue;
1349
+ const line = effect.value;
1350
+ if (line < 1 || line > tr.newDoc.lines) continue;
1351
+ return { effects: EditorView.scrollIntoView(tr.newDoc.line(line).from, { y: "nearest" }) };
1352
+ }
1353
+ return null;
1354
+ });
1355
+ /** One gutter for both: breakpoint markers and the current-line arrow (spec §6.1). */
1356
+ const debugGutter = gutter({
1357
+ class: "cm-stepcode-breakpoints",
1358
+ markers: (view) => view.state.field(breakpointField, false) ?? RangeSet.empty,
1359
+ lineMarker: (view, line) => {
1360
+ const current = view.state.field(currentLineField, false);
1361
+ return current !== void 0 && current !== null && current === line.from ? currentLineMarker : null;
1362
+ },
1363
+ lineMarkerChange: (update) => update.startState.field(currentLineField, false) !== update.state.field(currentLineField, false),
1364
+ initialSpacer: () => spacerMarker,
1365
+ domEventHandlers: { mousedown: toggleOnMouseDown }
1366
+ });
1367
+ /** Spec §6.1: only the primary (left) button toggles a breakpoint. */
1368
+ function toggleOnMouseDown(view, line, event) {
1369
+ if (event.button !== 0) return false;
1370
+ view.dispatch({ effects: toggleBreakpoint.of({ line: view.state.doc.lineAt(line.from).number }) });
1371
+ return true;
1372
+ }
1373
+ /**
1374
+ * Renders standalone: `stepcodeBaseTheme` rides along so its markers are visible even without
1375
+ * `stepcode()`. `EditorState` dedupes identical extension values by identity, so combining this
1376
+ * with `stepcode()` still installs the theme once.
1377
+ */
1378
+ function breakpoints() {
1379
+ return [
1380
+ breakpointField,
1381
+ debugGutter,
1382
+ stepcodeBaseTheme
1383
+ ];
1384
+ }
1385
+ /** Renders standalone; see `breakpoints()` on why `stepcodeBaseTheme` is included here too. */
1386
+ function currentLine() {
1387
+ return [
1388
+ currentLineField,
1389
+ scrollToCurrentLine,
1390
+ debugGutter,
1391
+ stepcodeBaseTheme
1392
+ ];
1393
+ }
1394
+ function debug() {
1395
+ return [breakpoints(), currentLine()];
1396
+ }
1397
+ /** 1-based, ascending; empty without the extension. */
1398
+ function breakpointLines(state) {
1399
+ const set = state.field(breakpointField, false);
1400
+ if (set === void 0) return [];
1401
+ return positionsOf(set).map((pos) => state.doc.lineAt(pos).number);
1402
+ }
1403
+ /** True when the update changed the breakpoint set — the host's cue to resend it. */
1404
+ function breakpointsChanged(update) {
1405
+ return update.startState.field(breakpointField, false) !== update.state.field(breakpointField, false);
1406
+ }
1407
+ /** 1-based, or null. */
1408
+ function currentLineOf(state) {
1409
+ const pos = state.field(currentLineField, false);
1410
+ return pos === void 0 || pos === null ? null : state.doc.lineAt(pos).number;
1411
+ }
1412
+ //#endregion
1413
+ //#region src/definition.ts
1414
+ /** The start of the declaration of the name at `pos`, or null (spec §5.10). */
1415
+ function definitionAt(state, pos) {
1416
+ const found = symbolAt(state, pos, 0);
1417
+ if (found === null || found.symbol.recovered === true) return null;
1418
+ return found.symbol.declaredAt.span.start;
1419
+ }
1420
+ const goToDefinition = (view) => {
1421
+ const target = definitionAt(view.state, view.state.selection.main.head);
1422
+ if (target === null) return false;
1423
+ view.dispatch({
1424
+ selection: { anchor: target },
1425
+ effects: EditorView.scrollIntoView(target, { y: "center" })
1426
+ });
1427
+ return true;
1428
+ };
1429
+ /** F12 only; a mouse gesture is the host's choice (spec §5.10). */
1430
+ const stepcodeKeymap = [{
1431
+ key: "F12",
1432
+ run: goToDefinition
1433
+ }];
1434
+ //#endregion
1435
+ //#region src/hover.ts
1436
+ const KEYWORD_BY_NODE = new Map(KEYWORD_KEYS.map((key) => [keywordNodeName(key), key]));
1437
+ /**
1438
+ * What a function returns. The subprogram symbol itself is never typed — the checker types the
1439
+ * result variable the header names, and type inference writes the inferred type there — so read
1440
+ * that symbol, falling back to the declared return type of the `f(): T` form.
1441
+ */
1442
+ function resultType(decl, result) {
1443
+ const named = decl.returnName === void 0 ? void 0 : result.symbols.get(decl.returnName);
1444
+ if (named !== void 0) return named.type;
1445
+ const ref = decl.returnType;
1446
+ if (ref === void 0) return UNKNOWN;
1447
+ return ref.dimensions.length === 0 ? scalar(ref.base) : arrayOf(ref.base, ref.dimensions.length);
1448
+ }
1449
+ /** `<kind> <name>: <type> (por referencia)` — the first hover line (spec §5.9). */
1450
+ function describe(symbol, result, options, strings) {
1451
+ const name = symbolLabel(symbol);
1452
+ const decl = symbol.decl;
1453
+ if (symbol.kind === "subprogram") {
1454
+ if (decl === void 0 || decl.form !== "function") return `${strings.procedure} ${name}`;
1455
+ return `${strings.function} ${name}: ${typeToString(resultType(decl, result), options.profile)}`;
1456
+ }
1457
+ const byRef = symbol.byRef === true ? ` (${strings.byReference})` : "";
1458
+ const type = typeToString(symbol.type, options.profile);
1459
+ return `${strings.kinds[symbol.kind]} ${name}: ${type}${byRef}`;
1460
+ }
1461
+ function hoverInfoAt(state, pos, side, options) {
1462
+ const strings = stringsFor(options.locale);
1463
+ const found = symbolAt(state, pos, side);
1464
+ if (found !== null) {
1465
+ const data = treeDataAt(state);
1466
+ if (data === null || found.symbol.recovered === true) return null;
1467
+ const line = new LineMap(data.result.source).positionAt(found.symbol.declaredAt.span.start).line;
1468
+ return {
1469
+ from: found.leaf.from,
1470
+ to: found.leaf.to,
1471
+ lines: [describe(found.symbol, data.result, options, strings), strings.declaredAt(line)]
1472
+ };
1473
+ }
1474
+ const node = syntaxTree(state).resolveInner(pos, side);
1475
+ const keyword = KEYWORD_BY_NODE.get(node.name);
1476
+ if (keyword !== void 0) return {
1477
+ from: node.from,
1478
+ to: node.to,
1479
+ lines: [state.doc.sliceString(node.from, node.to), strings.descriptions.keywords[keyword]]
1480
+ };
1481
+ if (node.name !== "BuiltinName") return null;
1482
+ const key = builtinKeyAt(options.profile, state.doc.sliceString(node.from, node.to));
1483
+ if (key === null) return null;
1484
+ const parts = builtinSignatureParts(key, options.profile, strings);
1485
+ return {
1486
+ from: node.from,
1487
+ to: node.to,
1488
+ lines: [signatureText(parts), strings.descriptions.builtins[key]]
1489
+ };
1490
+ }
1491
+ function hoverSource(options) {
1492
+ return (view, pos, side) => {
1493
+ const info = hoverInfoAt(view.state, pos, side, options);
1494
+ if (info === null) return null;
1495
+ return {
1496
+ pos: info.from,
1497
+ end: info.to,
1498
+ above: true,
1499
+ create: () => {
1500
+ const dom = document.createElement("div");
1501
+ dom.className = "cm-stepcode-hover";
1502
+ for (const line of info.lines) {
1503
+ const row = document.createElement("div");
1504
+ row.textContent = line;
1505
+ dom.appendChild(row);
1506
+ }
1507
+ return { dom };
1508
+ }
1509
+ };
1510
+ };
1511
+ }
1512
+ /** Spec §5.9. */
1513
+ function stepcodeHover(options) {
1514
+ return hoverTooltip(hoverSource(options));
1515
+ }
1516
+ //#endregion
1517
+ //#region src/lint.ts
1518
+ /**
1519
+ * Spec §5.2: a zero-width span is widened one character to the right, or to the left at the
1520
+ * end of its line, so the squiggle is visible; an empty line or document stays empty.
1521
+ */
1522
+ function widen(state, span) {
1523
+ if (span.end > span.start) return {
1524
+ from: span.start,
1525
+ to: span.end
1526
+ };
1527
+ const line = state.doc.lineAt(span.start);
1528
+ if (span.start < line.to) return {
1529
+ from: span.start,
1530
+ to: span.start + 1
1531
+ };
1532
+ if (span.start > line.from) return {
1533
+ from: span.start - 1,
1534
+ to: span.start
1535
+ };
1536
+ return {
1537
+ from: span.start,
1538
+ to: span.start
1539
+ };
1540
+ }
1541
+ /** The tree's compile diagnostics as CodeMirror diagnostics; empty before the first parse. */
1542
+ function stepcodeDiagnostics(state, options) {
1543
+ const data = treeDataAt(state);
1544
+ if (data === null) return [];
1545
+ const strings = stringsFor(options.locale);
1546
+ return data.result.diagnostics.map((diagnostic) => {
1547
+ const { from, to } = widen(state, diagnostic.span);
1548
+ const base = {
1549
+ from,
1550
+ to,
1551
+ severity: diagnostic.severity,
1552
+ source: diagnostic.code,
1553
+ message: formatDiagnostic(diagnostic, options.locale, options.profile)
1554
+ };
1555
+ const suggestion = diagnostic.data.suggestion;
1556
+ if (typeof suggestion !== "string") return base;
1557
+ return {
1558
+ ...base,
1559
+ actions: [{
1560
+ name: strings.replaceWith(suggestion),
1561
+ apply: (view, actionFrom, actionTo) => {
1562
+ view.dispatch({ changes: {
1563
+ from: actionFrom,
1564
+ to: actionTo,
1565
+ insert: suggestion
1566
+ } });
1567
+ }
1568
+ }]
1569
+ };
1570
+ });
1571
+ }
1572
+ /** Lint from the tree, re-run after every completed parse. */
1573
+ function stepcodeLint(options) {
1574
+ return linter((view) => stepcodeDiagnostics(view.state, options), {
1575
+ delay: 250,
1576
+ needsRefresh: (update) => syntaxTree(update.state) !== syntaxTree(update.startState)
1577
+ });
1578
+ }
1579
+ //#endregion
1580
+ //#region src/matching.ts
1581
+ /**
1582
+ * Spec §5.5: the stock matcher. Keyword, parenthesis, and bracket pairs all come from the
1583
+ * `closedBy` / `openedBy` props on the leaves (spec §4.2); the `brackets` text config stays as
1584
+ * the fallback for text the tree does not type.
1585
+ */
1586
+ function stepcodeBlockMatching() {
1587
+ return bracketMatching({ brackets: "()[]" });
1588
+ }
1589
+ //#endregion
1590
+ //#region src/signature.ts
1591
+ /** The innermost call whose argument list contains `pos` (spec §5.8), with its parenthesis. */
1592
+ function callAround(state, pos) {
1593
+ for (let node = syntaxTree(state).resolveInner(pos, -1); node !== null; node = node.parent) {
1594
+ if (node.name !== "Call" && node.name !== "BuiltinCall") continue;
1595
+ const open = node.getChild("OpenParen");
1596
+ if (open === null || open.to > pos) continue;
1597
+ const close = node.getChild("CloseParen");
1598
+ if (close !== null && pos > close.from) continue;
1599
+ return {
1600
+ node,
1601
+ open
1602
+ };
1603
+ }
1604
+ return null;
1605
+ }
1606
+ /** The number of argument separators of `call` that end at or before `pos`. */
1607
+ function activeArgument(state, call, pos) {
1608
+ let count = 0;
1609
+ for (const punct of call.getChildren("Punct")) if (punct.to <= pos && state.doc.sliceString(punct.from, punct.to) === ",") count++;
1610
+ return count;
1611
+ }
1612
+ /**
1613
+ * Where a header stops: after its parameter list's closing parenthesis. The search stays on
1614
+ * the line the parameters end on, and a declaration written without a parameter list at all
1615
+ * ends at its name — otherwise a parenthesis in the body would be taken for the list's.
1616
+ */
1617
+ function headerEnd(decl, source) {
1618
+ const last = decl.params[decl.params.length - 1];
1619
+ const paramsEnd = last === void 0 ? decl.name.span.end : last.span.end;
1620
+ const newline = source.indexOf("\n", paramsEnd);
1621
+ const rest = source.slice(paramsEnd, newline < 0 ? source.length : newline);
1622
+ if (last === void 0 && !rest.trimStart().startsWith("(")) return paramsEnd;
1623
+ const closeParen = rest.indexOf(")");
1624
+ return closeParen < 0 ? paramsEnd : paramsEnd + closeParen + 1;
1625
+ }
1626
+ /** The header of a declaration, its parameters split out so one can be marked active. */
1627
+ function headerParts(decl, source, active) {
1628
+ const end = headerEnd(decl, source);
1629
+ const parts = [];
1630
+ let cursor = decl.span.start;
1631
+ decl.params.forEach((param, index) => {
1632
+ if (param.span.start > cursor) parts.push({
1633
+ text: source.slice(cursor, param.span.start),
1634
+ active: false
1635
+ });
1636
+ parts.push({
1637
+ text: source.slice(param.span.start, param.span.end),
1638
+ active: index === active
1639
+ });
1640
+ cursor = param.span.end;
1641
+ });
1642
+ parts.push({
1643
+ text: source.slice(cursor, end),
1644
+ active: false
1645
+ });
1646
+ return parts;
1647
+ }
1648
+ function signatureAt(state, pos, options) {
1649
+ const data = treeDataAt(state);
1650
+ const found = callAround(state, pos);
1651
+ if (data === null || found === null) return null;
1652
+ const call = data.calls.get(found.node.from);
1653
+ if (call === void 0) return null;
1654
+ const active = activeArgument(state, found.node, pos);
1655
+ if (call.kind === "BuiltinCall") {
1656
+ const parts = builtinSignatureParts(call.key, options.profile, stringsFor(options.locale), active);
1657
+ return {
1658
+ pos: found.open.from,
1659
+ parts
1660
+ };
1661
+ }
1662
+ const decl = data.result.calls.get(call);
1663
+ if (decl === void 0) return null;
1664
+ return {
1665
+ pos: found.open.from,
1666
+ parts: headerParts(decl, data.result.source, active)
1667
+ };
1668
+ }
1669
+ function tooltipsFor(state, options) {
1670
+ const signature = signatureAt(state, state.selection.main.head, options);
1671
+ if (signature === null) return [];
1672
+ return [{
1673
+ pos: signature.pos,
1674
+ above: true,
1675
+ create: () => {
1676
+ const dom = document.createElement("div");
1677
+ dom.className = "cm-stepcode-signature";
1678
+ for (const part of signature.parts) {
1679
+ const span = document.createElement("span");
1680
+ if (part.active) span.className = "cm-stepcode-signature-active";
1681
+ span.textContent = part.text;
1682
+ dom.appendChild(span);
1683
+ }
1684
+ return { dom };
1685
+ }
1686
+ }];
1687
+ }
1688
+ /** Spec §5.8: a tooltip field recomputed on selection, document and tree changes. */
1689
+ function stepcodeSignatureHelp(options) {
1690
+ return StateField.define({
1691
+ create: (state) => tooltipsFor(state, options),
1692
+ update: (value, tr) => tr.docChanged || tr.selection !== void 0 || syntaxTree(tr.state) !== syntaxTree(tr.startState) ? tooltipsFor(tr.state, options) : value,
1693
+ provide: (field) => showTooltip.computeN([field], (state) => state.field(field))
1694
+ });
1695
+ }
1696
+ //#endregion
1697
+ //#region src/stepcode.ts
1698
+ /**
1699
+ * Spec §7: everything for one profile. Deliberately absent: a highlight style, the lint
1700
+ * gutter, line numbers, history and the default keymap — those are the host's.
1701
+ */
1702
+ function stepcode(options) {
1703
+ const resolved = {
1704
+ profile: options.profile,
1705
+ locale: options.locale ?? options.profile.locale
1706
+ };
1707
+ return new LanguageSupport(stepcodeLanguage(resolved.profile), [
1708
+ stepcodeLint(resolved),
1709
+ stepcodeCompletion(resolved),
1710
+ stepcodeSignatureHelp(resolved),
1711
+ stepcodeHover(resolved),
1712
+ stepcodeBlockMatching(),
1713
+ ...options.completion === false ? [] : [autocompletion()],
1714
+ ...options.arrow === false ? [] : [arrowInput(resolved.profile)],
1715
+ indentOnInput(),
1716
+ foldGutter(),
1717
+ keymap.of(stepcodeKeymap),
1718
+ stepcodeBaseTheme
1719
+ ]);
1720
+ }
1721
+ //#endregion
1722
+ export { arrowInput, breakpointLines, breakpoints, breakpointsChanged, compileResultAt, currentLine, currentLineOf, debug, goToDefinition, setBreakpoints, setCurrentLine, stepcode, stepcodeBlockMatching, stepcodeCompletion, stepcodeDiagnostics, stepcodeHover, stepcodeKeymap, stepcodeLanguage, stepcodeLint, stepcodeSignatureHelp, toggleBreakpoint, treeDataAt };