rapydscript-ns 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. package/.agignore +1 -0
  2. package/.gitattributes +4 -0
  3. package/.github/workflows/ci.yml +38 -0
  4. package/.github/workflows/web-repl-page-deploy.yml +42 -0
  5. package/=template.pyj +5 -0
  6. package/CHANGELOG.md +456 -0
  7. package/CONTRIBUTORS +13 -0
  8. package/HACKING.md +103 -0
  9. package/LICENSE +24 -0
  10. package/README.md +2512 -0
  11. package/TODO.md +327 -0
  12. package/add-toc-to-readme +2 -0
  13. package/bin/export +75 -0
  14. package/bin/rapydscript +70 -0
  15. package/bin/web-repl-export +102 -0
  16. package/build +3 -0
  17. package/package.json +46 -0
  18. package/publish.py +37 -0
  19. package/release/baselib-plain-pretty.js +4370 -0
  20. package/release/baselib-plain-ugly.js +3 -0
  21. package/release/compiler.js +18394 -0
  22. package/release/signatures.json +31 -0
  23. package/session.vim +4 -0
  24. package/setup.cfg +2 -0
  25. package/src/ast.pyj +1356 -0
  26. package/src/baselib-builtins.pyj +279 -0
  27. package/src/baselib-containers.pyj +723 -0
  28. package/src/baselib-errors.pyj +37 -0
  29. package/src/baselib-internal.pyj +421 -0
  30. package/src/baselib-itertools.pyj +97 -0
  31. package/src/baselib-str.pyj +798 -0
  32. package/src/compiler.pyj +36 -0
  33. package/src/errors.pyj +30 -0
  34. package/src/lib/aes.pyj +646 -0
  35. package/src/lib/collections.pyj +695 -0
  36. package/src/lib/elementmaker.pyj +83 -0
  37. package/src/lib/encodings.pyj +126 -0
  38. package/src/lib/functools.pyj +148 -0
  39. package/src/lib/gettext.pyj +569 -0
  40. package/src/lib/itertools.pyj +580 -0
  41. package/src/lib/math.pyj +193 -0
  42. package/src/lib/numpy.pyj +2101 -0
  43. package/src/lib/operator.pyj +11 -0
  44. package/src/lib/pythonize.pyj +20 -0
  45. package/src/lib/random.pyj +118 -0
  46. package/src/lib/re.pyj +470 -0
  47. package/src/lib/traceback.pyj +63 -0
  48. package/src/lib/uuid.pyj +77 -0
  49. package/src/monaco-language-service/analyzer.js +526 -0
  50. package/src/monaco-language-service/builtins.js +543 -0
  51. package/src/monaco-language-service/completions.js +498 -0
  52. package/src/monaco-language-service/diagnostics.js +643 -0
  53. package/src/monaco-language-service/dts.js +550 -0
  54. package/src/monaco-language-service/hover.js +121 -0
  55. package/src/monaco-language-service/index.js +386 -0
  56. package/src/monaco-language-service/scope.js +162 -0
  57. package/src/monaco-language-service/signature.js +144 -0
  58. package/src/output/__init__.pyj +0 -0
  59. package/src/output/classes.pyj +296 -0
  60. package/src/output/codegen.pyj +492 -0
  61. package/src/output/comments.pyj +45 -0
  62. package/src/output/exceptions.pyj +105 -0
  63. package/src/output/functions.pyj +491 -0
  64. package/src/output/literals.pyj +109 -0
  65. package/src/output/loops.pyj +444 -0
  66. package/src/output/modules.pyj +329 -0
  67. package/src/output/operators.pyj +429 -0
  68. package/src/output/statements.pyj +463 -0
  69. package/src/output/stream.pyj +309 -0
  70. package/src/output/treeshake.pyj +182 -0
  71. package/src/output/utils.pyj +72 -0
  72. package/src/parse.pyj +3106 -0
  73. package/src/string_interpolation.pyj +72 -0
  74. package/src/tokenizer.pyj +702 -0
  75. package/src/unicode_aliases.pyj +576 -0
  76. package/src/utils.pyj +192 -0
  77. package/test/_import_one.pyj +37 -0
  78. package/test/_import_two/__init__.pyj +11 -0
  79. package/test/_import_two/level2/__init__.pyj +0 -0
  80. package/test/_import_two/level2/deep.pyj +4 -0
  81. package/test/_import_two/other.pyj +6 -0
  82. package/test/_import_two/sub.pyj +13 -0
  83. package/test/aes_vectors.pyj +421 -0
  84. package/test/annotations.pyj +80 -0
  85. package/test/baselib.pyj +319 -0
  86. package/test/classes.pyj +452 -0
  87. package/test/collections.pyj +152 -0
  88. package/test/decorators.pyj +77 -0
  89. package/test/dict_spread.pyj +76 -0
  90. package/test/docstrings.pyj +39 -0
  91. package/test/elementmaker_test.pyj +45 -0
  92. package/test/ellipsis.pyj +49 -0
  93. package/test/functions.pyj +151 -0
  94. package/test/generators.pyj +41 -0
  95. package/test/generic.pyj +370 -0
  96. package/test/imports.pyj +72 -0
  97. package/test/internationalization.pyj +73 -0
  98. package/test/lint.pyj +164 -0
  99. package/test/loops.pyj +85 -0
  100. package/test/numpy.pyj +734 -0
  101. package/test/omit_function_metadata.pyj +20 -0
  102. package/test/regexp.pyj +55 -0
  103. package/test/repl.pyj +121 -0
  104. package/test/scoped_flags.pyj +76 -0
  105. package/test/starargs.pyj +506 -0
  106. package/test/starred_assign.pyj +104 -0
  107. package/test/str.pyj +198 -0
  108. package/test/subscript_tuple.pyj +53 -0
  109. package/test/unit/fixtures/fibonacci_expected.js +46 -0
  110. package/test/unit/index.js +2989 -0
  111. package/test/unit/language-service-builtins.js +815 -0
  112. package/test/unit/language-service-completions.js +1067 -0
  113. package/test/unit/language-service-dts.js +543 -0
  114. package/test/unit/language-service-hover.js +455 -0
  115. package/test/unit/language-service-scope.js +833 -0
  116. package/test/unit/language-service-signature.js +458 -0
  117. package/test/unit/language-service.js +705 -0
  118. package/test/unit/run-language-service.js +41 -0
  119. package/test/unit/web-repl.js +484 -0
  120. package/tools/build-language-service.js +190 -0
  121. package/tools/cli.js +547 -0
  122. package/tools/compile.js +219 -0
  123. package/tools/compiler.js +108 -0
  124. package/tools/completer.js +131 -0
  125. package/tools/embedded_compiler.js +251 -0
  126. package/tools/export.js +316 -0
  127. package/tools/gettext.js +185 -0
  128. package/tools/ini.js +65 -0
  129. package/tools/lint.js +705 -0
  130. package/tools/msgfmt.js +187 -0
  131. package/tools/repl.js +223 -0
  132. package/tools/self.js +162 -0
  133. package/tools/test.js +118 -0
  134. package/tools/utils.js +128 -0
  135. package/tools/web_repl.js +95 -0
  136. package/try +41 -0
  137. package/web-repl/env.js +74 -0
  138. package/web-repl/index.html +163 -0
  139. package/web-repl/language-service.js +4084 -0
  140. package/web-repl/main.js +254 -0
  141. package/web-repl/prism.css +139 -0
  142. package/web-repl/prism.js +113 -0
  143. package/web-repl/rapydscript.js +435 -0
  144. package/web-repl/sha1.js +25 -0
@@ -0,0 +1,386 @@
1
+ // index.js — RapydScript Monaco Language Service
2
+ // Public API: registerRapydScript(monaco, options) → service
3
+ //
4
+ // Usage:
5
+ // import { registerRapydScript } from './monaco-language-service/index.js';
6
+ //
7
+ // const service = registerRapydScript(monaco, {
8
+ // compiler: window.RapydScript, // compiled rapydscript bundle
9
+ // virtualFiles: { 'mymod': 'def f(): pass' }, // optional
10
+ // dtsFiles: [{ name: 'dom', content: '...' }], // optional (Phase 6)
11
+ // parseDelay: 300, // ms debounce (default: 300)
12
+ // });
13
+ //
14
+ // service.setVirtualFiles({ 'mymod': newSource });
15
+ // service.addDts('name', dtsText); // Phase 6 stub for now
16
+ // service.dispose(); // remove all Monaco providers
17
+
18
+ import { Diagnostics, BASE_BUILTINS } from './diagnostics.js';
19
+ import { SourceAnalyzer } from './analyzer.js';
20
+ import { CompletionEngine } from './completions.js';
21
+ import { SignatureHelpEngine } from './signature.js';
22
+ import { HoverEngine } from './hover.js';
23
+ import { DtsRegistry } from './dts.js';
24
+ import { BuiltinsRegistry } from './builtins.js';
25
+
26
+ const LANGUAGE_ID = 'rapydscript';
27
+
28
+ // ---------------------------------------------------------------------------
29
+ // Per-model state
30
+ // ---------------------------------------------------------------------------
31
+
32
+ class ModelState {
33
+ constructor(model, service) {
34
+ this._model = model;
35
+ this._service = service;
36
+ this._timer = null;
37
+ this._scopeMap = null; // most recent ScopeMap for this model
38
+
39
+ // Run immediately on first attach
40
+ this._schedule(0);
41
+
42
+ // Re-run on every edit
43
+ this._subscription = model.onDidChangeContent(() => {
44
+ this._schedule(service._parseDelay);
45
+ });
46
+ }
47
+
48
+ _schedule(delay) {
49
+ clearTimeout(this._timer);
50
+ this._timer = setTimeout(() => this._run(), delay);
51
+ }
52
+
53
+ _run() {
54
+ const service = this._service;
55
+ const code = this._model.getValue();
56
+ const opts = { virtualFiles: service._virtualFiles };
57
+
58
+ // Diagnostics (syntax errors + lint markers)
59
+ const markers = service._diagnostics.check(code, {
60
+ ...opts,
61
+ markerSeverity: service._monaco.MarkerSeverity,
62
+ });
63
+ service._monaco.editor.setModelMarkers(this._model, LANGUAGE_ID, markers);
64
+
65
+ // Scope analysis (feeds Phase 3 completions and Phase 5 hover)
66
+ this._scopeMap = service._analyzer.analyze(code, opts);
67
+ }
68
+
69
+ dispose() {
70
+ clearTimeout(this._timer);
71
+ this._subscription.dispose();
72
+ // Clear markers when detaching
73
+ this._service._monaco.editor.setModelMarkers(this._model, LANGUAGE_ID, []);
74
+ }
75
+ }
76
+
77
+ // ---------------------------------------------------------------------------
78
+ // RapydScriptLanguageService
79
+ // ---------------------------------------------------------------------------
80
+
81
+ class RapydScriptLanguageService {
82
+ constructor(monaco, options) {
83
+ this._monaco = monaco;
84
+ this._parseDelay = options.parseDelay !== undefined ? options.parseDelay : 300;
85
+ this._virtualFiles = Object.assign({}, options.virtualFiles || {});
86
+ this._disposables = [];
87
+ this._modelStates = new Map(); // model → ModelState
88
+
89
+ // Build compiler reference — accept either the raw RapydScript global
90
+ // or a web_repl instance (we only need the parse-level API).
91
+ const compiler = options.compiler;
92
+ if (!compiler) throw new Error('registerRapydScript: options.compiler is required');
93
+
94
+ this._diagnostics = new Diagnostics(compiler, options.extraBuiltins, options.pythonFlags);
95
+ this._analyzer = new SourceAnalyzer(compiler, options.pythonFlags);
96
+
97
+ // Preserve extra builtin names so they survive addDts() rebuilds
98
+ this._extraBuiltinNames = options.extraBuiltins ? Object.keys(options.extraBuiltins) : [];
99
+ this._stdlibFiles = Object.assign({}, options.stdlibFiles || {});
100
+
101
+ // DTS registry — load any files supplied at construction time
102
+ this._dts = new DtsRegistry();
103
+ if (options.dtsFiles) {
104
+ options.dtsFiles.forEach(f => this._dts.addDts(f.name, f.content));
105
+ }
106
+
107
+ // Built-in stubs registry (always present)
108
+ this._builtins = new BuiltinsRegistry();
109
+ this._builtins.enablePythonizeStrings();
110
+
111
+ // Merge BASE_BUILTINS + extra globals + DTS globals for completions
112
+ const builtin_names = BASE_BUILTINS
113
+ .concat(this._extraBuiltinNames)
114
+ .concat(this._dts.getGlobalNames());
115
+
116
+ this._completions = new CompletionEngine(this._analyzer, {
117
+ virtualFiles: this._virtualFiles,
118
+ stdlibFiles: this._stdlibFiles,
119
+ builtinNames: builtin_names,
120
+ dtsRegistry: this._dts,
121
+ builtinsRegistry: this._builtins,
122
+ });
123
+ this._signature_help = new SignatureHelpEngine(this._builtins);
124
+ this._hover = new HoverEngine(this._dts, this._builtins);
125
+
126
+ // Suppress diagnostics warnings for DTS globals
127
+ if (this._dts.getGlobalNames().length) {
128
+ this._diagnostics.addGlobals(this._dts.getGlobalNames());
129
+ }
130
+
131
+ // Store loadDts callback for lazy loading
132
+ this._loadDts = options.loadDts || null;
133
+
134
+ // Register language id if not already registered
135
+ const existing = monaco.languages.getLanguages().find(l => l.id === LANGUAGE_ID);
136
+ if (!existing) monaco.languages.register({ id: LANGUAGE_ID });
137
+
138
+ // Apply Python syntax highlighting — RapydScript is syntactically Python-like
139
+ // enough that Monaco's built-in Python tokenizer gives correct coloring.
140
+ // We then inject game-specific token rules so the same theme colors used for
141
+ // JS/TS files (ns, netscriptfunction, otherkeyvars, this) apply here too.
142
+ const extra_names = this._extraBuiltinNames;
143
+ const pythonLang = monaco.languages.getLanguages().find(l => l.id === 'python');
144
+ if (pythonLang && pythonLang.loader) {
145
+ pythonLang.loader().then(function(mod) {
146
+ const lang = mod.language;
147
+
148
+ // Python equivalents of the JS/TS otherkeyvars / this custom tokens.
149
+ // Unshift in reverse-priority order (last unshift = highest priority).
150
+ lang.tokenizer.root.unshift(
151
+ // self → 'this' color (Python's equivalent of JS `this`)
152
+ [/\bself\b/, { token: 'this' }],
153
+ // True / False / None / ... → 'otherkeyvars' (accent color, distinct from keywords)
154
+ [/\b(True|False|None)\b/, { token: 'otherkeyvars' }],
155
+ [/\.\.\./, { token: 'otherkeyvars' }],
156
+ );
157
+
158
+ // ns → 'ns' token (the game API namespace object)
159
+ lang.tokenizer.root.unshift([/\bns\b/, { token: 'ns' }]);
160
+
161
+ // extraBuiltins → 'netscriptfunction' token (all NS API names)
162
+ if (extra_names.length) {
163
+ const alt = extra_names
164
+ .map(n => n.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
165
+ .join('|');
166
+ lang.tokenizer.root.unshift([
167
+ new RegExp('\\b(?:' + alt + ')\\b'),
168
+ { token: 'netscriptfunction' },
169
+ ]);
170
+ }
171
+
172
+ monaco.languages.setMonarchTokensProvider(LANGUAGE_ID, lang);
173
+ monaco.languages.setLanguageConfiguration(LANGUAGE_ID, mod.conf);
174
+ });
175
+ }
176
+
177
+ // Attach to all currently open rapydscript models
178
+ monaco.editor.getModels().forEach(model => this._maybeAttach(model));
179
+
180
+ // Attach to future models
181
+ this._disposables.push(
182
+ monaco.editor.onDidCreateModel(model => this._maybeAttach(model))
183
+ );
184
+
185
+ // Detach when a model is disposed
186
+ this._disposables.push(
187
+ monaco.editor.onWillDisposeModel(model => this._detach(model))
188
+ );
189
+
190
+ // Register completion provider
191
+ const self = this;
192
+ this._disposables.push(
193
+ monaco.languages.registerCompletionItemProvider(LANGUAGE_ID, {
194
+ triggerCharacters: ['.'],
195
+ provideCompletionItems(model, position) {
196
+ if (model.getLanguageId() !== LANGUAGE_ID) return { suggestions: [] };
197
+ const state = self._modelStates.get(model);
198
+ const scopeMap = state ? state._scopeMap : null;
199
+ const line_content = model.getLineContent(position.lineNumber);
200
+ const line_prefix = line_content.substring(0, position.column - 1);
201
+ return self._completions.getCompletions(
202
+ scopeMap,
203
+ position,
204
+ line_prefix,
205
+ monaco.languages.CompletionItemKind
206
+ );
207
+ },
208
+ })
209
+ );
210
+
211
+ // Register signature help provider
212
+ this._disposables.push(
213
+ monaco.languages.registerSignatureHelpProvider(LANGUAGE_ID, {
214
+ signatureHelpTriggerCharacters: ['(', ','],
215
+ signatureHelpRetriggerCharacters: [','],
216
+ provideSignatureHelp(model, position) {
217
+ if (model.getLanguageId() !== LANGUAGE_ID) return null;
218
+ const state = self._modelStates.get(model);
219
+ const scopeMap = state ? state._scopeMap : null;
220
+ const line_content = model.getLineContent(position.lineNumber);
221
+ const line_prefix = line_content.substring(0, position.column - 1);
222
+ return self._signature_help.getSignatureHelp(
223
+ scopeMap,
224
+ position,
225
+ line_prefix
226
+ );
227
+ },
228
+ })
229
+ );
230
+
231
+ // Register hover provider
232
+ this._disposables.push(
233
+ monaco.languages.registerHoverProvider(LANGUAGE_ID, {
234
+ provideHover(model, position) {
235
+ if (model.getLanguageId() !== LANGUAGE_ID) return null;
236
+ const state = self._modelStates.get(model);
237
+ const scopeMap = state ? state._scopeMap : null;
238
+ const word_info = model.getWordAtPosition(position);
239
+ const word = word_info ? word_info.word : null;
240
+ const line_content = model.getLineContent(position.lineNumber);
241
+ // Text before the word's start column, used to detect dot-chain access
242
+ const line_before_word = word_info
243
+ ? line_content.substring(0, word_info.startColumn - 1)
244
+ : '';
245
+ return self._hover.getHover(scopeMap, position, word, line_before_word);
246
+ },
247
+ })
248
+ );
249
+ }
250
+
251
+ // ---- internal ----------------------------------------------------------
252
+
253
+ _maybeAttach(model) {
254
+ if (model.getLanguageId() !== LANGUAGE_ID) return;
255
+ if (this._modelStates.has(model)) return;
256
+ this._modelStates.set(model, new ModelState(model, this));
257
+ }
258
+
259
+ _detach(model) {
260
+ const state = this._modelStates.get(model);
261
+ if (!state) return;
262
+ state.dispose();
263
+ this._modelStates.delete(model);
264
+ }
265
+
266
+ // ---- public API --------------------------------------------------------
267
+
268
+ /**
269
+ * Replace / merge virtual files available to the import resolver.
270
+ * Triggers an immediate re-check of all open models.
271
+ * @param {Object.<string,string>} files
272
+ */
273
+ setVirtualFiles(files) {
274
+ Object.assign(this._virtualFiles, files);
275
+ this._completions.setVirtualFiles(this._virtualFiles);
276
+ this._modelStates.forEach(state => state._schedule(0));
277
+ }
278
+
279
+ /**
280
+ * Remove a virtual file by name.
281
+ * @param {string} name
282
+ */
283
+ removeVirtualFile(name) {
284
+ delete this._virtualFiles[name];
285
+ this._completions.setVirtualFiles(this._virtualFiles);
286
+ this._modelStates.forEach(state => state._schedule(0));
287
+ }
288
+
289
+ /**
290
+ * Add additional global symbol names (e.g. from d.ts) so they are not
291
+ * flagged as undefined. Phase 6 will call this automatically.
292
+ * @param {string[]} names
293
+ */
294
+ addGlobals(names) {
295
+ this._diagnostics.addGlobals(names);
296
+ this._modelStates.forEach(state => state._schedule(0));
297
+ }
298
+
299
+ /**
300
+ * Return the most recently built ScopeMap for a given Monaco model.
301
+ * Returns null if the model is not attached or has not been analysed yet.
302
+ * Used by Phase 3 (completions) and Phase 5 (hover).
303
+ * @param {object} model - Monaco ITextModel
304
+ * @returns {import('./scope.js').ScopeMap|null}
305
+ */
306
+ getScopeMap(model) {
307
+ const state = this._modelStates.get(model);
308
+ return state ? state._scopeMap : null;
309
+ }
310
+
311
+ /**
312
+ * Stub for Phase 6 (DtsRegistry). Calling it is safe but has no effect yet.
313
+ * @param {string} _name
314
+ * @param {string} _dtsText
315
+ */
316
+ addDts(name, dtsText) {
317
+ this._dts.addDts(name, dtsText);
318
+ const new_names = this._dts.getGlobalNames();
319
+ // Suppress undef warnings for newly added globals
320
+ this._diagnostics.addGlobals(new_names);
321
+ // Rebuild builtin list so completions include new DTS names (preserve extraBuiltins)
322
+ const builtin_names = BASE_BUILTINS.concat(this._extraBuiltinNames).concat(new_names);
323
+ this._completions = new CompletionEngine(this._analyzer, {
324
+ virtualFiles: this._virtualFiles,
325
+ stdlibFiles: this._stdlibFiles,
326
+ builtinNames: builtin_names,
327
+ dtsRegistry: this._dts,
328
+ builtinsRegistry: this._builtins,
329
+ });
330
+ // Re-run diagnostics on all open models
331
+ this._modelStates.forEach(state => state._schedule(0));
332
+ }
333
+
334
+ /**
335
+ * Asynchronously fetch a .d.ts file using the `loadDts` callback supplied
336
+ * in options, then register it with addDts().
337
+ *
338
+ * Requires `options.loadDts` to have been provided at construction time.
339
+ * Returns a Promise that resolves when the file has been parsed and registered.
340
+ *
341
+ * @param {string} name identifier passed to the loadDts callback
342
+ * @returns {Promise<void>}
343
+ */
344
+ loadDts(name) {
345
+ if (!this._loadDts) {
346
+ return Promise.reject(
347
+ new Error('registerRapydScript: options.loadDts was not provided')
348
+ );
349
+ }
350
+ const self = this;
351
+ return Promise.resolve(this._loadDts(name)).then(function (text) {
352
+ self.addDts(name, text);
353
+ });
354
+ }
355
+
356
+ /**
357
+ * Remove all Monaco providers and event listeners registered by this service.
358
+ */
359
+ dispose() {
360
+ this._modelStates.forEach(state => state.dispose());
361
+ this._modelStates.clear();
362
+ this._disposables.forEach(d => d.dispose());
363
+ this._disposables = [];
364
+ }
365
+ }
366
+
367
+ // ---------------------------------------------------------------------------
368
+ // Exported factory
369
+ // ---------------------------------------------------------------------------
370
+
371
+ /**
372
+ * Register the RapydScript language service with a Monaco instance.
373
+ *
374
+ * @param {object} monaco - the Monaco global (window.monaco or require('monaco-editor'))
375
+ * @param {object} options
376
+ * @param {object} options.compiler - window.RapydScript compiled bundle (required)
377
+ * @param {object} [options.virtualFiles] - map of module-name → source
378
+ * @param {Array} [options.dtsFiles] - [{name, content}] d.ts files (Phase 6)
379
+ * @param {number} [options.parseDelay=300] - debounce ms
380
+ * @param {object} [options.extraBuiltins] - extra {name: true} globals to suppress undef warnings
381
+ * @param {string} [options.pythonFlags] - comma-separated python flags (e.g. "dict_literals,overload_getitem")
382
+ * @returns {RapydScriptLanguageService}
383
+ */
384
+ export function registerRapydScript(monaco, options) {
385
+ return new RapydScriptLanguageService(monaco, options || {});
386
+ }
@@ -0,0 +1,162 @@
1
+ // scope.js — ScopeMap data structures for the RapydScript language service.
2
+ //
3
+ // Position convention throughout this file:
4
+ // line — 1-indexed (matches Monaco's lineNumber)
5
+ // column — 1-indexed (matches Monaco's column)
6
+ // AST tokens use 1-indexed lines and 0-indexed cols; the analyzer converts
7
+ // before storing so callers always work with fully 1-indexed numbers.
8
+
9
+ // ---------------------------------------------------------------------------
10
+ // SymbolInfo
11
+ // ---------------------------------------------------------------------------
12
+
13
+ export class SymbolInfo {
14
+ /**
15
+ * @param {object} opts
16
+ * @param {string} opts.name
17
+ * @param {string} opts.kind 'function'|'method'|'class'|'variable'|'parameter'|'import'
18
+ * @param {{line:number,column:number}} opts.defined_at 1-indexed line and column
19
+ * @param {number} opts.scope_depth
20
+ * @param {string|null} [opts.doc]
21
+ * @param {Array|null} [opts.params] [{name, is_rest, is_kwargs}] for functions/methods
22
+ */
23
+ constructor(opts) {
24
+ this.name = opts.name;
25
+ this.kind = opts.kind;
26
+ this.defined_at = opts.defined_at; // {line, column} both 1-indexed
27
+ this.scope_depth = opts.scope_depth;
28
+ this.doc = opts.doc || null;
29
+ this.params = opts.params || null;
30
+ this.inferred_class = opts.inferred_class || null; // 'ClassName' when x = ClassName(...)
31
+ this.dts_call_path = opts.dts_call_path || null; // 'ns.getServer' when x = ns.getServer(...)
32
+ this.type = null; // Phase 6: TypeInfo
33
+ }
34
+ }
35
+
36
+ // ---------------------------------------------------------------------------
37
+ // ScopeFrame
38
+ // ---------------------------------------------------------------------------
39
+
40
+ export class ScopeFrame {
41
+ /**
42
+ * @param {object} opts
43
+ * @param {string} opts.kind 'module'|'function'|'class'|'comprehension'|'block'
44
+ * @param {string|null} opts.name
45
+ * @param {{start:{line,column}, end:{line,column}}} opts.range 1-indexed
46
+ * @param {ScopeFrame|null} opts.parent
47
+ * @param {number} opts.depth
48
+ */
49
+ constructor(opts) {
50
+ this.kind = opts.kind;
51
+ this.name = opts.name || null;
52
+ this.range = opts.range;
53
+ this.parent = opts.parent || null;
54
+ this.depth = opts.depth;
55
+ this.symbols = new Map(); // name → SymbolInfo
56
+ }
57
+
58
+ /** Add or overwrite a symbol in this frame. */
59
+ addSymbol(sym) {
60
+ this.symbols.set(sym.name, sym);
61
+ }
62
+
63
+ /** Return a symbol by exact name, or null. */
64
+ getSymbol(name) {
65
+ return this.symbols.get(name) || null;
66
+ }
67
+ }
68
+
69
+ // ---------------------------------------------------------------------------
70
+ // Range helpers (all positions 1-indexed)
71
+ // ---------------------------------------------------------------------------
72
+
73
+ function range_contains(range, line, column) {
74
+ const { start, end } = range;
75
+ const after_start = (line > start.line) || (line === start.line && column >= start.column);
76
+ const before_end = (line < end.line) || (line === end.line && column <= end.column);
77
+ return after_start && before_end;
78
+ }
79
+
80
+ // ---------------------------------------------------------------------------
81
+ // ScopeMap
82
+ // ---------------------------------------------------------------------------
83
+
84
+ export class ScopeMap {
85
+ constructor() {
86
+ /** All frames in document order (outer to inner). @type {ScopeFrame[]} */
87
+ this.frames = [];
88
+ }
89
+
90
+ /** @internal Called by ScopeBuilder as frames are created. */
91
+ addFrame(frame) {
92
+ this.frames.push(frame);
93
+ }
94
+
95
+ /**
96
+ * Return all ScopeFrames whose range contains (line, column), innermost first.
97
+ * Both line and column are 1-indexed (Monaco format).
98
+ * @param {number} line
99
+ * @param {number} column
100
+ * @returns {ScopeFrame[]}
101
+ */
102
+ getScopesAtPosition(line, column) {
103
+ return this.frames
104
+ .filter(f => range_contains(f.range, line, column))
105
+ .sort((a, b) => b.depth - a.depth);
106
+ }
107
+
108
+ /**
109
+ * Return every symbol visible at (line, column).
110
+ * When two scopes define the same name, the innermost one wins.
111
+ * Both line and column are 1-indexed (Monaco format).
112
+ * @param {number} line
113
+ * @param {number} column
114
+ * @returns {SymbolInfo[]}
115
+ */
116
+ getSymbolsAtPosition(line, column) {
117
+ const scopes = this.getScopesAtPosition(line, column);
118
+ const seen = new Set();
119
+ const result = [];
120
+ for (const scope of scopes) {
121
+ for (const [name, sym] of scope.symbols) {
122
+ if (!seen.has(name)) {
123
+ seen.add(name);
124
+ result.push(sym);
125
+ }
126
+ }
127
+ }
128
+ return result;
129
+ }
130
+
131
+ /**
132
+ * Look up a single symbol by name that is visible at (line, column).
133
+ * Returns the innermost definition, or null if not found.
134
+ * Both line and column are 1-indexed (Monaco format).
135
+ * @param {string} name
136
+ * @param {number} line
137
+ * @param {number} column
138
+ * @returns {SymbolInfo|null}
139
+ */
140
+ getSymbol(name, line, column) {
141
+ const scopes = this.getScopesAtPosition(line, column);
142
+ for (const scope of scopes) {
143
+ const sym = scope.getSymbol(name);
144
+ if (sym) return sym;
145
+ }
146
+ return null;
147
+ }
148
+
149
+ /**
150
+ * Return all symbols across every scope in the document.
151
+ * @returns {SymbolInfo[]}
152
+ */
153
+ getAllSymbols() {
154
+ const result = [];
155
+ for (const frame of this.frames) {
156
+ for (const sym of frame.symbols.values()) {
157
+ result.push(sym);
158
+ }
159
+ }
160
+ return result;
161
+ }
162
+ }
@@ -0,0 +1,144 @@
1
+ // signature.js — Signature help provider for the RapydScript language service.
2
+ //
3
+ // Usage:
4
+ // import { SignatureHelpEngine, detect_call_context } from './signature.js';
5
+ // const engine = new SignatureHelpEngine();
6
+ // const help = engine.getSignatureHelp(scopeMap, position, linePrefix);
7
+
8
+ // ---------------------------------------------------------------------------
9
+ // Context detection
10
+ // ---------------------------------------------------------------------------
11
+
12
+ /**
13
+ * Walk backwards through linePrefix to find the nearest unclosed `(`.
14
+ * Returns the callee name and which argument position the cursor is in,
15
+ * or null if the cursor is not inside a call.
16
+ *
17
+ * @param {string} linePrefix text from column 1 up to (but not including) the cursor
18
+ * @returns {{ callee: string, activeParameter: number }|null}
19
+ */
20
+ export function detect_call_context(linePrefix) {
21
+ let depth = 0;
22
+ let paren_pos = -1;
23
+
24
+ // Scan backwards to find the outermost unclosed '('
25
+ for (let i = linePrefix.length - 1; i >= 0; i--) {
26
+ const ch = linePrefix[i];
27
+ if (ch === ')') {
28
+ depth++;
29
+ } else if (ch === '(') {
30
+ if (depth === 0) {
31
+ paren_pos = i;
32
+ break;
33
+ }
34
+ depth--;
35
+ }
36
+ }
37
+
38
+ if (paren_pos < 0) return null; // not inside a call
39
+
40
+ // Extract the callee: identifier immediately before the '('
41
+ const before_paren = linePrefix.slice(0, paren_pos).trimEnd();
42
+ const callee_match = before_paren.match(/(\w+)$/);
43
+ if (!callee_match) return null;
44
+
45
+ // Count commas at depth 0 between the '(' and the cursor
46
+ let active_param = 0;
47
+ let inner_depth = 0;
48
+ for (let j = paren_pos + 1; j < linePrefix.length; j++) {
49
+ const ch = linePrefix[j];
50
+ if (ch === '(' || ch === '[') {
51
+ inner_depth++;
52
+ } else if (ch === ')' || ch === ']') {
53
+ inner_depth--;
54
+ } else if (ch === ',' && inner_depth === 0) {
55
+ active_param++;
56
+ }
57
+ }
58
+
59
+ return { callee: callee_match[1], activeParameter: active_param };
60
+ }
61
+
62
+ // ---------------------------------------------------------------------------
63
+ // SignatureHelpEngine
64
+ // ---------------------------------------------------------------------------
65
+
66
+ export class SignatureHelpEngine {
67
+
68
+ /**
69
+ * @param {import('./builtins.js').BuiltinsRegistry|null} [builtinsRegistry]
70
+ * Optional built-in stubs; used as fallback when the callee is not in ScopeMap.
71
+ */
72
+ constructor(builtinsRegistry) {
73
+ this._builtins = builtinsRegistry || null;
74
+ }
75
+
76
+ /**
77
+ * Return a Monaco ISignatureHelpResult for the cursor position, or null
78
+ * if the cursor is not inside a known function call.
79
+ *
80
+ * Priority: ScopeMap (user-defined) → built-in stubs.
81
+ *
82
+ * @param {import('./scope.js').ScopeMap|null} scopeMap
83
+ * @param {{lineNumber:number,column:number}} position 1-indexed Monaco position
84
+ * @param {string} linePrefix text on the current line up to the cursor
85
+ * @returns {{ value: object, dispose: function }|null}
86
+ */
87
+ getSignatureHelp(scopeMap, position, linePrefix) {
88
+ const ctx = detect_call_context(linePrefix);
89
+ if (!ctx) return null;
90
+
91
+ // 1. User-defined function from ScopeMap
92
+ if (scopeMap) {
93
+ const sym = scopeMap.getSymbol(ctx.callee, position.lineNumber, position.column);
94
+ if (sym && sym.params && sym.params.length > 0) {
95
+ const param_labels = sym.params.map(function (p) {
96
+ if (p.is_separator) return p.name; // '/' or '*'
97
+ if (p.is_kwargs) return '**' + p.name;
98
+ if (p.is_rest) return '*' + p.name;
99
+ return p.name;
100
+ });
101
+
102
+ let active = ctx.activeParameter;
103
+ if (active > param_labels.length - 1) active = param_labels.length - 1;
104
+
105
+ return {
106
+ value: {
107
+ signatures: [{
108
+ label: ctx.callee + '(' + param_labels.join(', ') + ')',
109
+ documentation: sym.doc || undefined,
110
+ parameters: param_labels.map(function (l) { return { label: l }; }),
111
+ }],
112
+ activeSignature: 0,
113
+ activeParameter: active,
114
+ },
115
+ dispose: function () {},
116
+ };
117
+ }
118
+ }
119
+
120
+ // 2. Built-in stubs
121
+ if (this._builtins) {
122
+ const info = this._builtins.getSignatureInfo(ctx.callee);
123
+ if (info) {
124
+ let active = ctx.activeParameter;
125
+ if (active > info.params.length - 1) active = info.params.length - 1;
126
+
127
+ return {
128
+ value: {
129
+ signatures: [{
130
+ label: info.label,
131
+ documentation: info.doc || undefined,
132
+ parameters: info.params,
133
+ }],
134
+ activeSignature: 0,
135
+ activeParameter: active,
136
+ },
137
+ dispose: function () {},
138
+ };
139
+ }
140
+ }
141
+
142
+ return null;
143
+ }
144
+ }
File without changes