tova 0.2.9 → 0.3.1
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/bin/tova.js +1404 -114
- package/package.json +3 -1
- package/src/analyzer/analyzer.js +882 -695
- package/src/analyzer/client-analyzer.js +191 -0
- package/src/analyzer/server-analyzer.js +467 -0
- package/src/analyzer/types.js +20 -4
- package/src/codegen/base-codegen.js +473 -111
- package/src/codegen/client-codegen.js +109 -46
- package/src/codegen/codegen.js +65 -5
- package/src/codegen/server-codegen.js +297 -38
- package/src/diagnostics/error-codes.js +255 -0
- package/src/diagnostics/formatter.js +150 -28
- package/src/docs/generator.js +390 -0
- package/src/lexer/lexer.js +306 -64
- package/src/lexer/tokens.js +19 -0
- package/src/lsp/server.js +935 -53
- package/src/parser/ast.js +81 -368
- package/src/parser/client-ast.js +138 -0
- package/src/parser/client-parser.js +504 -0
- package/src/parser/parser.js +492 -1056
- package/src/parser/server-ast.js +240 -0
- package/src/parser/server-parser.js +602 -0
- package/src/runtime/array-proto.js +32 -0
- package/src/runtime/embedded.js +1 -1
- package/src/runtime/reactivity.js +239 -42
- package/src/stdlib/advanced-collections.js +81 -0
- package/src/stdlib/inline.js +556 -13
- package/src/version.js +1 -1
package/src/lsp/server.js
CHANGED
|
@@ -8,12 +8,13 @@ import { Analyzer } from '../analyzer/analyzer.js';
|
|
|
8
8
|
import { TokenType } from '../lexer/tokens.js';
|
|
9
9
|
import { Formatter } from '../formatter/formatter.js';
|
|
10
10
|
import { TypeRegistry } from '../analyzer/type-registry.js';
|
|
11
|
+
import { BUILTIN_NAMES, BUILTIN_FUNCTIONS } from '../stdlib/inline.js';
|
|
11
12
|
|
|
12
13
|
class TovaLanguageServer {
|
|
13
14
|
static MAX_CACHE_SIZE = 100; // max cached diagnostics entries
|
|
14
15
|
|
|
15
16
|
constructor() {
|
|
16
|
-
this._buffer =
|
|
17
|
+
this._buffer = Buffer.alloc(0);
|
|
17
18
|
this._documents = new Map(); // uri -> { text, version }
|
|
18
19
|
this._diagnosticsCache = new Map(); // uri -> { ast, analyzer, errors, typeRegistry }
|
|
19
20
|
this._initialized = false;
|
|
@@ -22,7 +23,7 @@ class TovaLanguageServer {
|
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
start() {
|
|
25
|
-
|
|
26
|
+
// Do NOT set encoding — use raw Buffers for correct byte-based Content-Length (LSP protocol)
|
|
26
27
|
process.stdin.on('data', (chunk) => this._onData(chunk));
|
|
27
28
|
process.stdin.on('end', () => process.exit(0));
|
|
28
29
|
|
|
@@ -46,12 +47,13 @@ class TovaLanguageServer {
|
|
|
46
47
|
// ─── JSON-RPC Transport ────────────────────────────────────
|
|
47
48
|
|
|
48
49
|
_onData(chunk) {
|
|
49
|
-
this._buffer
|
|
50
|
+
this._buffer = Buffer.concat([this._buffer, typeof chunk === 'string' ? Buffer.from(chunk, 'utf8') : chunk]);
|
|
50
51
|
while (true) {
|
|
51
|
-
const
|
|
52
|
+
const sep = Buffer.from('\r\n\r\n');
|
|
53
|
+
const headerEnd = this._buffer.indexOf(sep);
|
|
52
54
|
if (headerEnd === -1) break;
|
|
53
55
|
|
|
54
|
-
const header = this._buffer.slice(0, headerEnd);
|
|
56
|
+
const header = this._buffer.slice(0, headerEnd).toString('utf8');
|
|
55
57
|
const match = header.match(/Content-Length:\s*(\d+)/i);
|
|
56
58
|
if (!match) {
|
|
57
59
|
this._buffer = this._buffer.slice(headerEnd + 4);
|
|
@@ -62,7 +64,7 @@ class TovaLanguageServer {
|
|
|
62
64
|
const start = headerEnd + 4;
|
|
63
65
|
if (this._buffer.length < start + contentLength) break;
|
|
64
66
|
|
|
65
|
-
const body = this._buffer.slice(start, start + contentLength);
|
|
67
|
+
const body = this._buffer.slice(start, start + contentLength).toString('utf8');
|
|
66
68
|
this._buffer = this._buffer.slice(start + contentLength);
|
|
67
69
|
|
|
68
70
|
try {
|
|
@@ -116,7 +118,9 @@ class TovaLanguageServer {
|
|
|
116
118
|
case 'textDocument/signatureHelp': return this._onSignatureHelp(msg);
|
|
117
119
|
case 'textDocument/formatting': return this._onFormatting(msg);
|
|
118
120
|
case 'textDocument/rename': return this._onRename(msg);
|
|
121
|
+
case 'textDocument/codeAction': return this._onCodeAction(msg);
|
|
119
122
|
case 'textDocument/references': return this._onReferences(msg);
|
|
123
|
+
case 'textDocument/inlayHint': return this._onInlayHint(msg);
|
|
120
124
|
case 'workspace/symbol': return this._onWorkspaceSymbol(msg);
|
|
121
125
|
default: return this._respondError(msg.id, -32601, `Method not found: ${method}`);
|
|
122
126
|
}
|
|
@@ -153,9 +157,13 @@ class TovaLanguageServer {
|
|
|
153
157
|
signatureHelpProvider: {
|
|
154
158
|
triggerCharacters: ['(', ','],
|
|
155
159
|
},
|
|
160
|
+
codeActionProvider: {
|
|
161
|
+
codeActionKinds: ['quickfix'],
|
|
162
|
+
},
|
|
156
163
|
documentFormattingProvider: true,
|
|
157
164
|
renameProvider: { prepareProvider: false },
|
|
158
165
|
referencesProvider: true,
|
|
166
|
+
inlayHintProvider: true,
|
|
159
167
|
workspaceSymbolProvider: true,
|
|
160
168
|
},
|
|
161
169
|
});
|
|
@@ -241,8 +249,9 @@ class TovaLanguageServer {
|
|
|
241
249
|
},
|
|
242
250
|
severity,
|
|
243
251
|
source: 'tova',
|
|
244
|
-
message: w.message,
|
|
252
|
+
message: w.hint ? `${w.message} (hint: ${w.hint})` : w.message,
|
|
245
253
|
};
|
|
254
|
+
if (w.code) diag.code = w.code;
|
|
246
255
|
// Add unnecessary tag for unused variables
|
|
247
256
|
if (w.message.includes('declared but never used')) {
|
|
248
257
|
diag.tags = [1]; // Unnecessary
|
|
@@ -305,7 +314,7 @@ class TovaLanguageServer {
|
|
|
305
314
|
diagnostics.push({
|
|
306
315
|
range: {
|
|
307
316
|
start: { line: (e.line || 1) - 1, character: (e.column || 1) - 1 },
|
|
308
|
-
end: { line: (e.line || 1) - 1, character: (e.column || 1) + 10 },
|
|
317
|
+
end: { line: (e.line || 1) - 1, character: (e.column || 1) - 1 + 10 },
|
|
309
318
|
},
|
|
310
319
|
severity: 1,
|
|
311
320
|
source: 'tova',
|
|
@@ -332,6 +341,8 @@ class TovaLanguageServer {
|
|
|
332
341
|
if (msg.includes('Non-exhaustive match')) return 2;
|
|
333
342
|
// Type mismatches in strict mode → Error (1)
|
|
334
343
|
if (msg.includes('Type mismatch')) return 1;
|
|
344
|
+
// Naming convention → Hint (4)
|
|
345
|
+
if (msg.includes('should use snake_case') || msg.includes('should use PascalCase')) return 4;
|
|
335
346
|
// Default → Warning (2)
|
|
336
347
|
return 2;
|
|
337
348
|
}
|
|
@@ -414,7 +425,7 @@ class TovaLanguageServer {
|
|
|
414
425
|
|
|
415
426
|
// Keywords
|
|
416
427
|
const keywords = [
|
|
417
|
-
'fn', 'let', 'if', 'elif', 'else', 'for', 'while', 'in',
|
|
428
|
+
'fn', 'let', 'if', 'elif', 'else', 'for', 'while', 'loop', 'when', 'in',
|
|
418
429
|
'return', 'match', 'type', 'import', 'from', 'true', 'false',
|
|
419
430
|
'nil', 'server', 'client', 'shared', 'pub', 'mut',
|
|
420
431
|
'try', 'catch', 'finally', 'break', 'continue', 'async', 'await',
|
|
@@ -426,15 +437,17 @@ class TovaLanguageServer {
|
|
|
426
437
|
}
|
|
427
438
|
}
|
|
428
439
|
|
|
429
|
-
// Built-in functions
|
|
430
|
-
const
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
440
|
+
// Built-in functions (dynamically from stdlib) — with parameter info
|
|
441
|
+
for (const fn of BUILTIN_NAMES) {
|
|
442
|
+
if (fn.startsWith(prefix) && !fn.startsWith('__')) {
|
|
443
|
+
const detail = this._getBuiltinDetail(fn);
|
|
444
|
+
items.push({ label: fn, kind: 3 /* Function */, detail });
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
// Runtime types
|
|
448
|
+
for (const rt of ['Ok', 'Err', 'Some', 'None']) {
|
|
449
|
+
if (rt.startsWith(prefix)) {
|
|
450
|
+
items.push({ label: rt, kind: 3 /* Function */, detail: 'Tova built-in' });
|
|
438
451
|
}
|
|
439
452
|
}
|
|
440
453
|
|
|
@@ -443,7 +456,7 @@ class TovaLanguageServer {
|
|
|
443
456
|
if (cached?.analyzer) {
|
|
444
457
|
const symbols = this._collectSymbols(cached.analyzer);
|
|
445
458
|
for (const sym of symbols) {
|
|
446
|
-
if (sym.name.startsWith(prefix) && !
|
|
459
|
+
if (sym.name.startsWith(prefix) && !BUILTIN_NAMES.has(sym.name)) {
|
|
447
460
|
items.push({
|
|
448
461
|
label: sym.name,
|
|
449
462
|
kind: sym.kind === 'function' ? 3 : sym.kind === 'type' ? 22 : 6,
|
|
@@ -617,23 +630,291 @@ class TovaLanguageServer {
|
|
|
617
630
|
const word = this._getWordAt(line, position.character);
|
|
618
631
|
if (!word) return this._respond(msg.id, null);
|
|
619
632
|
|
|
620
|
-
// Check builtins
|
|
633
|
+
// Check builtins — comprehensive hover docs for all stdlib functions
|
|
621
634
|
const builtinDocs = {
|
|
635
|
+
// Core
|
|
622
636
|
'print': '`fn print(...args)` — Print values to console',
|
|
623
637
|
'len': '`fn len(v)` — Get length of string, array, or object',
|
|
624
|
-
'range': '`fn range(start, end
|
|
638
|
+
'range': '`fn range(start, end?, step?)` — Generate array of numbers',
|
|
625
639
|
'enumerate': '`fn enumerate(arr)` — Returns [[index, value], ...]',
|
|
626
|
-
'
|
|
627
|
-
|
|
628
|
-
'
|
|
629
|
-
'
|
|
630
|
-
'
|
|
631
|
-
'
|
|
632
|
-
|
|
633
|
-
'
|
|
634
|
-
'
|
|
635
|
-
'
|
|
636
|
-
'
|
|
640
|
+
'type_of': '`fn type_of(v) -> String` — Get Tova type name as string',
|
|
641
|
+
// Result/Option
|
|
642
|
+
'Ok': '`Ok(value) -> Result` — Create a successful Result\n\nMethods: `.map(fn)`, `.flatMap(fn)`, `.andThen(fn)`, `.unwrap()`, `.unwrapOr(default)`, `.isOk()`, `.isErr()`, `.mapErr(fn)`',
|
|
643
|
+
'Err': '`Err(error) -> Result` — Create an error Result\n\nMethods: `.unwrapOr(default)`, `.isOk()`, `.isErr()`, `.mapErr(fn)`, `.unwrapErr()`',
|
|
644
|
+
'Some': '`Some(value) -> Option` — Create an Option with a value\n\nMethods: `.map(fn)`, `.flatMap(fn)`, `.andThen(fn)`, `.unwrap()`, `.unwrapOr(default)`, `.isSome()`, `.isNone()`, `.filter(fn)`',
|
|
645
|
+
'None': '`None` — Empty Option value\n\nMethods: `.unwrapOr(default)`, `.isSome()`, `.isNone()`',
|
|
646
|
+
// Collections
|
|
647
|
+
'filter': '`fn filter(arr, fn) -> [T]` — Filter array by predicate',
|
|
648
|
+
'map': '`fn map(arr, fn) -> [U]` — Transform each element',
|
|
649
|
+
'find': '`fn find(arr, fn) -> T?` — Find first matching element (nil if none)',
|
|
650
|
+
'find_index': '`fn find_index(arr, fn) -> Int?` — Find index of first match (nil if none)',
|
|
651
|
+
'any': '`fn any(arr, fn) -> Bool` — True if any element matches predicate',
|
|
652
|
+
'all': '`fn all(arr, fn) -> Bool` — True if all elements match predicate',
|
|
653
|
+
'flat_map': '`fn flat_map(arr, fn) -> [U]` — Map and flatten one level',
|
|
654
|
+
'reduce': '`fn reduce(arr, fn, init?) -> T` — Reduce array to single value',
|
|
655
|
+
'sum': '`fn sum(arr) -> Float` — Sum all elements in array',
|
|
656
|
+
'min': '`fn min(arr) -> T?` — Minimum value in array',
|
|
657
|
+
'max': '`fn max(arr) -> T?` — Maximum value in array',
|
|
658
|
+
'sorted': '`fn sorted(arr, key?) -> [T]` — Return sorted copy of array',
|
|
659
|
+
'reversed': '`fn reversed(arr) -> [T]` — Return reversed copy',
|
|
660
|
+
'zip': '`fn zip(...arrays) -> [[T]]` — Zip arrays together',
|
|
661
|
+
'unique': '`fn unique(arr) -> [T]` — Remove duplicates',
|
|
662
|
+
'group_by': '`fn group_by(arr, fn) -> {String: [T]}` — Group elements by key function',
|
|
663
|
+
'chunk': '`fn chunk(arr, n) -> [[T]]` — Split array into chunks of size n',
|
|
664
|
+
'flatten': '`fn flatten(arr) -> [T]` — Flatten one level of nesting',
|
|
665
|
+
'take': '`fn take(arr, n) -> [T]` — Take first n elements',
|
|
666
|
+
'drop': '`fn drop(arr, n) -> [T]` — Drop first n elements',
|
|
667
|
+
'first': '`fn first(arr) -> T?` — First element (nil if empty)',
|
|
668
|
+
'last': '`fn last(arr) -> T?` — Last element (nil if empty)',
|
|
669
|
+
'count': '`fn count(arr, fn) -> Int` — Count elements matching predicate',
|
|
670
|
+
'partition': '`fn partition(arr, fn) -> [[T], [T]]` — Split into [matching, non-matching]',
|
|
671
|
+
'includes': '`fn includes(arr, value) -> Bool` — Check if array contains value',
|
|
672
|
+
'compact': '`fn compact(arr) -> [T]` — Remove nil values',
|
|
673
|
+
'rotate': '`fn rotate(arr, n) -> [T]` — Rotate array by n positions',
|
|
674
|
+
'insert_at': '`fn insert_at(arr, idx, val) -> [T]` — Insert value at index',
|
|
675
|
+
'remove_at': '`fn remove_at(arr, idx) -> [T]` — Remove element at index',
|
|
676
|
+
'update_at': '`fn update_at(arr, idx, val) -> [T]` — Replace element at index',
|
|
677
|
+
// Math
|
|
678
|
+
'abs': '`fn abs(n) -> Float` — Absolute value',
|
|
679
|
+
'floor': '`fn floor(n) -> Int` — Round down',
|
|
680
|
+
'ceil': '`fn ceil(n) -> Int` — Round up',
|
|
681
|
+
'round': '`fn round(n) -> Int` — Round to nearest integer',
|
|
682
|
+
'clamp': '`fn clamp(n, lo, hi) -> Float` — Clamp value to range [lo, hi]',
|
|
683
|
+
'sqrt': '`fn sqrt(n) -> Float` — Square root',
|
|
684
|
+
'pow': '`fn pow(base, exp) -> Float` — Exponentiation',
|
|
685
|
+
'random': '`fn random() -> Float` — Random number in [0, 1)',
|
|
686
|
+
'random_int': '`fn random_int(lo, hi) -> Int` — Random integer in [lo, hi]',
|
|
687
|
+
'random_float': '`fn random_float(lo, hi) -> Float` — Random float in [lo, hi)',
|
|
688
|
+
'sign': '`fn sign(n) -> Int` — Sign of number (-1, 0, or 1)',
|
|
689
|
+
'trunc': '`fn trunc(n) -> Int` — Truncate toward zero',
|
|
690
|
+
'gcd': '`fn gcd(a, b) -> Int` — Greatest common divisor',
|
|
691
|
+
'lcm': '`fn lcm(a, b) -> Int` — Least common multiple',
|
|
692
|
+
'factorial': '`fn factorial(n) -> Int` — Factorial (nil for negative)',
|
|
693
|
+
'hypot': '`fn hypot(a, b) -> Float` — Hypotenuse length',
|
|
694
|
+
'lerp': '`fn lerp(a, b, t) -> Float` — Linear interpolation',
|
|
695
|
+
'divmod': '`fn divmod(a, b) -> [Int, Int]` — Quotient and remainder',
|
|
696
|
+
'avg': '`fn avg(arr) -> Float` — Average of array values',
|
|
697
|
+
'is_nan': '`fn is_nan(n) -> Bool` — Check if value is NaN',
|
|
698
|
+
'is_finite': '`fn is_finite(n) -> Bool` — Check if value is finite',
|
|
699
|
+
'is_close': '`fn is_close(a, b, tol?) -> Bool` — Check if values are approximately equal',
|
|
700
|
+
'PI': '`PI: Float` — Mathematical constant pi (3.14159...)',
|
|
701
|
+
'E': '`E: Float` — Euler\'s number (2.71828...)',
|
|
702
|
+
'INF': '`INF: Float` — Positive infinity',
|
|
703
|
+
// Trig
|
|
704
|
+
'sin': '`fn sin(n) -> Float` — Sine (radians)',
|
|
705
|
+
'cos': '`fn cos(n) -> Float` — Cosine (radians)',
|
|
706
|
+
'tan': '`fn tan(n) -> Float` — Tangent (radians)',
|
|
707
|
+
'asin': '`fn asin(n) -> Float` — Arcsine',
|
|
708
|
+
'acos': '`fn acos(n) -> Float` — Arccosine',
|
|
709
|
+
'atan': '`fn atan(n) -> Float` — Arctangent',
|
|
710
|
+
'atan2': '`fn atan2(y, x) -> Float` — Two-argument arctangent',
|
|
711
|
+
'to_radians': '`fn to_radians(deg) -> Float` — Convert degrees to radians',
|
|
712
|
+
'to_degrees': '`fn to_degrees(rad) -> Float` — Convert radians to degrees',
|
|
713
|
+
// Logarithmic
|
|
714
|
+
'log': '`fn log(n) -> Float` — Natural logarithm',
|
|
715
|
+
'log2': '`fn log2(n) -> Float` — Base-2 logarithm',
|
|
716
|
+
'log10': '`fn log10(n) -> Float` — Base-10 logarithm',
|
|
717
|
+
'exp': '`fn exp(n) -> Float` — e raised to the power n',
|
|
718
|
+
// String
|
|
719
|
+
'trim': '`fn trim(s) -> String` — Remove leading/trailing whitespace',
|
|
720
|
+
'trim_start': '`fn trim_start(s) -> String` — Remove leading whitespace',
|
|
721
|
+
'trim_end': '`fn trim_end(s) -> String` — Remove trailing whitespace',
|
|
722
|
+
'split': '`fn split(s, sep) -> [String]` — Split string by separator',
|
|
723
|
+
'join': '`fn join(arr, sep) -> String` — Join array elements with separator',
|
|
724
|
+
'replace': '`fn replace(s, from, to) -> String` — Replace all occurrences',
|
|
725
|
+
'replace_first': '`fn replace_first(s, from, to) -> String` — Replace first occurrence',
|
|
726
|
+
'repeat': '`fn repeat(s, n) -> String` — Repeat string n times',
|
|
727
|
+
'upper': '`fn upper(s) -> String` — Convert to uppercase',
|
|
728
|
+
'lower': '`fn lower(s) -> String` — Convert to lowercase',
|
|
729
|
+
'contains': '`fn contains(s, sub) -> Bool` — Check if string contains substring',
|
|
730
|
+
'starts_with': '`fn starts_with(s, prefix) -> Bool` — Check if string starts with prefix',
|
|
731
|
+
'ends_with': '`fn ends_with(s, suffix) -> Bool` — Check if string ends with suffix',
|
|
732
|
+
'chars': '`fn chars(s) -> [String]` — Split string into individual characters',
|
|
733
|
+
'words': '`fn words(s) -> [String]` — Split by whitespace',
|
|
734
|
+
'lines': '`fn lines(s) -> [String]` — Split by newlines',
|
|
735
|
+
'capitalize': '`fn capitalize(s) -> String` — Capitalize first letter',
|
|
736
|
+
'title_case': '`fn title_case(s) -> String` — Capitalize each word',
|
|
737
|
+
'snake_case': '`fn snake_case(s) -> String` — Convert to snake_case',
|
|
738
|
+
'camel_case': '`fn camel_case(s) -> String` — Convert to camelCase',
|
|
739
|
+
'kebab_case': '`fn kebab_case(s) -> String` — Convert to kebab-case',
|
|
740
|
+
'pad_start': '`fn pad_start(s, n, fill?) -> String` — Pad start to length n',
|
|
741
|
+
'pad_end': '`fn pad_end(s, n, fill?) -> String` — Pad end to length n',
|
|
742
|
+
'char_at': '`fn char_at(s, i) -> String?` — Character at index (nil if out of bounds)',
|
|
743
|
+
'index_of': '`fn index_of(s, sub) -> Int?` — Index of first occurrence (nil if not found)',
|
|
744
|
+
'last_index_of': '`fn last_index_of(s, sub) -> Int?` — Index of last occurrence',
|
|
745
|
+
'count_of': '`fn count_of(s, sub) -> Int` — Count occurrences of substring',
|
|
746
|
+
'reverse_str': '`fn reverse_str(s) -> String` — Reverse a string',
|
|
747
|
+
'substr': '`fn substr(s, start, end?) -> String` — Extract substring',
|
|
748
|
+
'center': '`fn center(s, n, fill?) -> String` — Center string in field of width n',
|
|
749
|
+
'slugify': '`fn slugify(s) -> String` — Convert to URL-safe slug',
|
|
750
|
+
'truncate': '`fn truncate(s, n, suffix?) -> String` — Truncate with ellipsis',
|
|
751
|
+
'escape_html': '`fn escape_html(s) -> String` — Escape HTML entities',
|
|
752
|
+
'unescape_html': '`fn unescape_html(s) -> String` — Unescape HTML entities',
|
|
753
|
+
'dedent': '`fn dedent(s) -> String` — Remove common leading whitespace',
|
|
754
|
+
'indent_str': '`fn indent_str(s, n, ch?) -> String` — Indent each line',
|
|
755
|
+
'word_wrap': '`fn word_wrap(s, width) -> String` — Wrap text at word boundaries',
|
|
756
|
+
'fmt': '`fn fmt(template, ...args) -> String` — Format string with `{}` placeholders',
|
|
757
|
+
'is_empty': '`fn is_empty(v) -> Bool` — Check if string, array, or object is empty',
|
|
758
|
+
// Object
|
|
759
|
+
'keys': '`fn keys(obj) -> [String]` — Object keys',
|
|
760
|
+
'values': '`fn values(obj) -> [T]` — Object values',
|
|
761
|
+
'entries': '`fn entries(obj) -> [[String, T]]` — Object entries as [key, value] pairs',
|
|
762
|
+
'merge': '`fn merge(...objs) -> Object` — Merge objects (later values win)',
|
|
763
|
+
'freeze': '`fn freeze(obj) -> Object` — Make object immutable',
|
|
764
|
+
'clone': '`fn clone(obj) -> Object` — Deep clone',
|
|
765
|
+
'has_key': '`fn has_key(obj, key) -> Bool` — Check if object has key',
|
|
766
|
+
'get': '`fn get(obj, path, default?) -> T` — Get nested value by dot path',
|
|
767
|
+
'pick': '`fn pick(obj, keys) -> Object` — Select subset of keys',
|
|
768
|
+
'omit': '`fn omit(obj, keys) -> Object` — Remove subset of keys',
|
|
769
|
+
'map_values': '`fn map_values(obj, fn) -> Object` — Transform all values',
|
|
770
|
+
'from_entries': '`fn from_entries(pairs) -> Object` — Create object from [key, value] pairs',
|
|
771
|
+
// Type Conversion
|
|
772
|
+
'to_int': '`fn to_int(v) -> Int?` — Parse value to integer (nil on failure)',
|
|
773
|
+
'to_float': '`fn to_float(v) -> Float?` — Parse value to float (nil on failure)',
|
|
774
|
+
'to_string': '`fn to_string(v) -> String` — Convert any value to string',
|
|
775
|
+
'to_bool': '`fn to_bool(v) -> Bool` — Convert value to boolean',
|
|
776
|
+
// Assertions
|
|
777
|
+
'assert': '`fn assert(cond, msg?)` — Assert condition is true',
|
|
778
|
+
'assert_eq': '`fn assert_eq(a, b, msg?)` — Assert values are equal',
|
|
779
|
+
'assert_ne': '`fn assert_ne(a, b, msg?)` — Assert values are not equal',
|
|
780
|
+
// Async
|
|
781
|
+
'sleep': '`fn sleep(ms) -> Promise` — Wait for ms milliseconds',
|
|
782
|
+
'parallel': '`fn parallel(list) -> Promise` — Run promises concurrently (Promise.all)',
|
|
783
|
+
'timeout': '`fn timeout(promise, ms) -> Promise` — Reject if promise exceeds timeout',
|
|
784
|
+
'retry': '`fn retry(fn, {times?, delay?, backoff?}) -> Promise` — Retry async function',
|
|
785
|
+
// Functional
|
|
786
|
+
'compose': '`fn compose(...fns) -> Function` — Right-to-left function composition',
|
|
787
|
+
'pipe_fn': '`fn pipe_fn(...fns) -> Function` — Left-to-right function composition',
|
|
788
|
+
'identity': '`fn identity(x) -> T` — Return value unchanged',
|
|
789
|
+
'memoize': '`fn memoize(fn) -> Function` — Cache function results',
|
|
790
|
+
'debounce': '`fn debounce(fn, ms) -> Function` — Debounce function calls',
|
|
791
|
+
'throttle': '`fn throttle(fn, ms) -> Function` — Throttle function calls',
|
|
792
|
+
'once': '`fn once(fn) -> Function` — Only call function once',
|
|
793
|
+
'negate': '`fn negate(fn) -> Function` — Return a negated predicate',
|
|
794
|
+
'partial': '`fn partial(fn, ...args) -> Function` — Partially apply arguments',
|
|
795
|
+
'curry': '`fn curry(fn, arity?) -> Function` — Curry a function',
|
|
796
|
+
'flip': '`fn flip(fn) -> Function` — Swap first two arguments',
|
|
797
|
+
// Error Handling
|
|
798
|
+
'try_fn': '`fn try_fn(fn) -> Result` — Wrap function call in Result',
|
|
799
|
+
'try_async': '`fn try_async(fn) -> Result` — Wrap async call in Result',
|
|
800
|
+
'filter_ok': '`fn filter_ok(arr) -> [T]` — Extract Ok values from Result array',
|
|
801
|
+
'filter_err': '`fn filter_err(arr) -> [E]` — Extract Err values from Result array',
|
|
802
|
+
// Randomness
|
|
803
|
+
'choice': '`fn choice(arr) -> T?` — Random element from array',
|
|
804
|
+
'sample': '`fn sample(arr, n) -> [T]` — Random n elements from array',
|
|
805
|
+
'shuffle': '`fn shuffle(arr) -> [T]` — Randomly reorder array',
|
|
806
|
+
// JSON
|
|
807
|
+
'json_parse': '`fn json_parse(s) -> Result` — Parse JSON string (returns Result)',
|
|
808
|
+
'json_stringify': '`fn json_stringify(v) -> String` — Convert to JSON string',
|
|
809
|
+
'json_pretty': '`fn json_pretty(v) -> String` — Convert to pretty-printed JSON',
|
|
810
|
+
// Encoding
|
|
811
|
+
'base64_encode': '`fn base64_encode(s) -> String` — Encode string to base64',
|
|
812
|
+
'base64_decode': '`fn base64_decode(s) -> String` — Decode base64 string',
|
|
813
|
+
'url_encode': '`fn url_encode(s) -> String` — URL-encode string',
|
|
814
|
+
'url_decode': '`fn url_decode(s) -> String` — URL-decode string',
|
|
815
|
+
'hex_encode': '`fn hex_encode(s) -> String` — Encode string to hex',
|
|
816
|
+
'hex_decode': '`fn hex_decode(s) -> String` — Decode hex string',
|
|
817
|
+
// Number Formatting
|
|
818
|
+
'format_number': '`fn format_number(n, {separator?, decimals?}) -> String` — Format number with separators',
|
|
819
|
+
'to_hex': '`fn to_hex(n) -> String` — Convert number to hex string',
|
|
820
|
+
'to_binary': '`fn to_binary(n) -> String` — Convert number to binary string',
|
|
821
|
+
'to_octal': '`fn to_octal(n) -> String` — Convert number to octal string',
|
|
822
|
+
'to_fixed': '`fn to_fixed(n, decimals) -> Float` — Round to fixed decimal places',
|
|
823
|
+
// Itertools
|
|
824
|
+
'pairwise': '`fn pairwise(arr) -> [[T, T]]` — Adjacent pairs',
|
|
825
|
+
'combinations': '`fn combinations(arr, r) -> [[T]]` — All r-combinations',
|
|
826
|
+
'permutations': '`fn permutations(arr, r?) -> [[T]]` — All permutations',
|
|
827
|
+
'intersperse': '`fn intersperse(arr, sep) -> [T]` — Insert separator between elements',
|
|
828
|
+
'interleave': '`fn interleave(...arrs) -> [T]` — Interleave multiple arrays',
|
|
829
|
+
'repeat_value': '`fn repeat_value(val, n) -> [T]` — Create array of n copies',
|
|
830
|
+
'sliding_window': '`fn sliding_window(arr, n) -> [[T]]` — Sliding window of size n',
|
|
831
|
+
'zip_with': '`fn zip_with(a, b, fn) -> [U]` — Zip two arrays with combining function',
|
|
832
|
+
'frequencies': '`fn frequencies(arr) -> {String: Int}` — Count occurrences of each element',
|
|
833
|
+
'scan': '`fn scan(arr, fn, init) -> [T]` — Running accumulation',
|
|
834
|
+
'min_by': '`fn min_by(arr, fn) -> T?` — Minimum by key function',
|
|
835
|
+
'max_by': '`fn max_by(arr, fn) -> T?` — Maximum by key function',
|
|
836
|
+
'sum_by': '`fn sum_by(arr, fn) -> Float` — Sum by key function',
|
|
837
|
+
'product': '`fn product(arr) -> Float` — Product of all elements',
|
|
838
|
+
'binary_search': '`fn binary_search(arr, target, key?) -> Int` — Binary search (-1 if not found)',
|
|
839
|
+
'is_sorted': '`fn is_sorted(arr, key?) -> Bool` — Check if array is sorted',
|
|
840
|
+
// Set Operations
|
|
841
|
+
'intersection': '`fn intersection(a, b) -> [T]` — Elements in both arrays',
|
|
842
|
+
'difference': '`fn difference(a, b) -> [T]` — Elements in a but not b',
|
|
843
|
+
'symmetric_difference': '`fn symmetric_difference(a, b) -> [T]` — Elements in either but not both',
|
|
844
|
+
'is_subset': '`fn is_subset(a, b) -> Bool` — Check if a is subset of b',
|
|
845
|
+
'is_superset': '`fn is_superset(a, b) -> Bool` — Check if a is superset of b',
|
|
846
|
+
// Statistics
|
|
847
|
+
'mean': '`fn mean(arr) -> Float` — Arithmetic mean',
|
|
848
|
+
'median': '`fn median(arr) -> Float?` — Median value',
|
|
849
|
+
'mode': '`fn mode(arr) -> T?` — Most frequent value',
|
|
850
|
+
'stdev': '`fn stdev(arr) -> Float` — Standard deviation',
|
|
851
|
+
'variance': '`fn variance(arr) -> Float` — Variance',
|
|
852
|
+
'percentile': '`fn percentile(arr, p) -> Float?` — p-th percentile',
|
|
853
|
+
// Validation
|
|
854
|
+
'is_email': '`fn is_email(s) -> Bool` — Check if string is valid email',
|
|
855
|
+
'is_url': '`fn is_url(s) -> Bool` — Check if string is valid URL',
|
|
856
|
+
'is_numeric': '`fn is_numeric(s) -> Bool` — Check if string is numeric',
|
|
857
|
+
'is_alpha': '`fn is_alpha(s) -> Bool` — Check if string is alphabetic',
|
|
858
|
+
'is_alphanumeric': '`fn is_alphanumeric(s) -> Bool` — Check if string is alphanumeric',
|
|
859
|
+
'is_uuid': '`fn is_uuid(s) -> Bool` — Check if string is valid UUID',
|
|
860
|
+
'is_hex': '`fn is_hex(s) -> Bool` — Check if string is valid hex',
|
|
861
|
+
// URL
|
|
862
|
+
'uuid': '`fn uuid() -> String` — Generate random UUID v4',
|
|
863
|
+
'parse_url': '`fn parse_url(s) -> Result` — Parse URL into components',
|
|
864
|
+
'build_url': '`fn build_url(parts) -> String` — Build URL from components',
|
|
865
|
+
'parse_query': '`fn parse_query(s) -> Object` — Parse query string',
|
|
866
|
+
'build_query': '`fn build_query(obj) -> String` — Build query string from object',
|
|
867
|
+
// Regex
|
|
868
|
+
'regex_test': '`fn regex_test(s, pattern, flags?) -> Bool` — Test if string matches regex',
|
|
869
|
+
'regex_match': '`fn regex_match(s, pattern, flags?) -> Result` — First regex match',
|
|
870
|
+
'regex_find_all': '`fn regex_find_all(s, pattern, flags?) -> [Match]` — All regex matches',
|
|
871
|
+
'regex_replace': '`fn regex_replace(s, pattern, replacement, flags?) -> String` — Replace by regex',
|
|
872
|
+
'regex_split': '`fn regex_split(s, pattern, flags?) -> [String]` — Split by regex',
|
|
873
|
+
'regex_capture': '`fn regex_capture(s, pattern, flags?) -> Result` — Named capture groups',
|
|
874
|
+
// Date/Time
|
|
875
|
+
'now': '`fn now() -> Int` — Current timestamp in milliseconds',
|
|
876
|
+
'now_iso': '`fn now_iso() -> String` — Current time as ISO 8601 string',
|
|
877
|
+
'date_parse': '`fn date_parse(s) -> Result` — Parse date string',
|
|
878
|
+
'date_format': '`fn date_format(d, fmt) -> String` — Format date (iso, date, time, datetime, or custom)',
|
|
879
|
+
'date_add': '`fn date_add(d, amount, unit) -> Date` — Add time to date',
|
|
880
|
+
'date_diff': '`fn date_diff(d1, d2, unit) -> Int` — Difference between dates',
|
|
881
|
+
'date_from': '`fn date_from(parts) -> Date` — Create date from {year, month, day, ...}',
|
|
882
|
+
'date_part': '`fn date_part(d, part) -> Int` — Extract part (year, month, day, ...)',
|
|
883
|
+
'time_ago': '`fn time_ago(d) -> String` — Human-readable relative time',
|
|
884
|
+
// I/O
|
|
885
|
+
'read': '`fn read(source, opts?) -> Table` — Read CSV, JSON, JSONL, or URL into Table',
|
|
886
|
+
'write': '`fn write(data, dest, opts?)` — Write Table/array to CSV, JSON, JSONL',
|
|
887
|
+
// Scripting
|
|
888
|
+
'env': '`fn env(key?, fallback?) -> String?` — Get environment variable',
|
|
889
|
+
'args': '`fn args() -> [String]` — Get CLI arguments',
|
|
890
|
+
'exit': '`fn exit(code?)` — Exit process',
|
|
891
|
+
'exists': '`fn exists(path) -> Bool` — Check if file/directory exists',
|
|
892
|
+
'read_text': '`fn read_text(path) -> Result` — Read file as string',
|
|
893
|
+
'write_text': '`fn write_text(path, content, opts?) -> Result` — Write string to file',
|
|
894
|
+
'mkdir': '`fn mkdir(dir) -> Result` — Create directory (recursive)',
|
|
895
|
+
'ls': '`fn ls(dir?, opts?) -> [String]` — List directory contents',
|
|
896
|
+
'cwd': '`fn cwd() -> String` — Current working directory',
|
|
897
|
+
'read_stdin': '`fn read_stdin() -> String` — Read all of stdin (for piped input)',
|
|
898
|
+
'read_lines': '`fn read_lines() -> [String]` — Read stdin as array of lines',
|
|
899
|
+
'script_path': '`fn script_path() -> String?` — Absolute path of running .tova script',
|
|
900
|
+
'script_dir': '`fn script_dir() -> String?` — Directory containing running .tova script',
|
|
901
|
+
'parse_args': '`fn parse_args(argv) -> {flags, positional}` — Parse CLI args into flags and positional args',
|
|
902
|
+
'color': '`fn color(text, name) -> String` — ANSI color (red/green/yellow/blue/magenta/cyan/white/gray)',
|
|
903
|
+
'bold': '`fn bold(text) -> String` — Bold ANSI text',
|
|
904
|
+
'dim': '`fn dim(text) -> String` — Dim ANSI text',
|
|
905
|
+
'on_signal': '`fn on_signal(name, callback)` — Register signal handler (e.g. "SIGINT")',
|
|
906
|
+
'file_stat': '`fn file_stat(path) -> Result<{size, mode, mtime, atime, isDir, isFile, isSymlink}>` — File metadata',
|
|
907
|
+
'file_size': '`fn file_size(path) -> Result<Int>` — File size in bytes',
|
|
908
|
+
'path_join': '`fn path_join(...parts) -> String` — Join path segments',
|
|
909
|
+
'path_dirname': '`fn path_dirname(path) -> String` — Directory portion of path',
|
|
910
|
+
'path_basename': '`fn path_basename(path, ext?) -> String` — File name portion of path',
|
|
911
|
+
'path_resolve': '`fn path_resolve(path) -> String` — Resolve to absolute path',
|
|
912
|
+
'path_ext': '`fn path_ext(path) -> String` — File extension (e.g. ".js")',
|
|
913
|
+
'path_relative': '`fn path_relative(from, to) -> String` — Relative path between two paths',
|
|
914
|
+
'symlink': '`fn symlink(target, path) -> Result` — Create symbolic link',
|
|
915
|
+
'readlink': '`fn readlink(path) -> Result<String>` — Read symbolic link target',
|
|
916
|
+
'is_symlink': '`fn is_symlink(path) -> Bool` — Check if path is a symbolic link',
|
|
917
|
+
'spawn': '`fn spawn(cmd, args?, opts?) -> Promise<Result<{stdout, stderr, exitCode}>>` — Async shell command',
|
|
637
918
|
};
|
|
638
919
|
|
|
639
920
|
if (builtinDocs[word]) {
|
|
@@ -714,11 +995,32 @@ class TovaLanguageServer {
|
|
|
714
995
|
const line = doc.text.split('\n')[position.line] || '';
|
|
715
996
|
const before = line.slice(0, position.character);
|
|
716
997
|
|
|
717
|
-
//
|
|
718
|
-
|
|
719
|
-
|
|
998
|
+
// Walk backwards to find the immediately enclosing function call (handles nesting)
|
|
999
|
+
let depth = 0;
|
|
1000
|
+
let parenPos = -1;
|
|
1001
|
+
for (let i = before.length - 1; i >= 0; i--) {
|
|
1002
|
+
if (before[i] === ')') depth++;
|
|
1003
|
+
else if (before[i] === '(') {
|
|
1004
|
+
if (depth === 0) { parenPos = i; break; }
|
|
1005
|
+
depth--;
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
if (parenPos === -1) return this._respond(msg.id, null);
|
|
720
1009
|
|
|
721
|
-
const
|
|
1010
|
+
const funcMatch = before.slice(0, parenPos).match(/(\w+)\s*$/);
|
|
1011
|
+
if (!funcMatch) return this._respond(msg.id, null);
|
|
1012
|
+
|
|
1013
|
+
const funcName = funcMatch[1];
|
|
1014
|
+
|
|
1015
|
+
// Count commas at depth 0 after the enclosing paren (ignores nested call commas)
|
|
1016
|
+
const afterParen = before.slice(parenPos + 1);
|
|
1017
|
+
let activeParam = 0;
|
|
1018
|
+
let parenDepth = 0;
|
|
1019
|
+
for (const ch of afterParen) {
|
|
1020
|
+
if (ch === '(') parenDepth++;
|
|
1021
|
+
else if (ch === ')') parenDepth--;
|
|
1022
|
+
else if (ch === ',' && parenDepth === 0) activeParam++;
|
|
1023
|
+
}
|
|
722
1024
|
|
|
723
1025
|
// Built-in signatures
|
|
724
1026
|
const signatures = {
|
|
@@ -737,10 +1039,6 @@ class TovaLanguageServer {
|
|
|
737
1039
|
|
|
738
1040
|
const sig = signatures[funcName];
|
|
739
1041
|
if (sig) {
|
|
740
|
-
// Count commas to determine active parameter
|
|
741
|
-
const afterParen = before.slice(before.lastIndexOf('(') + 1);
|
|
742
|
-
const activeParam = (afterParen.match(/,/g) || []).length;
|
|
743
|
-
|
|
744
1042
|
return this._respond(msg.id, {
|
|
745
1043
|
signatures: [{
|
|
746
1044
|
label: sig.label,
|
|
@@ -755,17 +1053,14 @@ class TovaLanguageServer {
|
|
|
755
1053
|
const cached = this._diagnosticsCache.get(textDocument.uri);
|
|
756
1054
|
if (cached?.analyzer) {
|
|
757
1055
|
const symbol = this._findSymbolInScopes(cached.analyzer, funcName);
|
|
758
|
-
if (symbol?.
|
|
759
|
-
const afterParen = before.slice(before.lastIndexOf('(') + 1);
|
|
760
|
-
const activeParam = (afterParen.match(/,/g) || []).length;
|
|
761
|
-
|
|
1056
|
+
if (symbol?._params) {
|
|
762
1057
|
return this._respond(msg.id, {
|
|
763
1058
|
signatures: [{
|
|
764
|
-
label: `${funcName}(${symbol.
|
|
765
|
-
parameters: symbol.
|
|
1059
|
+
label: `${funcName}(${symbol._params.join(', ')})`,
|
|
1060
|
+
parameters: symbol._params.map(p => ({ label: p })),
|
|
766
1061
|
}],
|
|
767
1062
|
activeSignature: 0,
|
|
768
|
-
activeParameter: Math.min(activeParam, symbol.
|
|
1063
|
+
activeParameter: Math.max(0, Math.min(activeParam, symbol._params.length - 1)),
|
|
769
1064
|
});
|
|
770
1065
|
}
|
|
771
1066
|
}
|
|
@@ -803,7 +1098,352 @@ class TovaLanguageServer {
|
|
|
803
1098
|
}
|
|
804
1099
|
}
|
|
805
1100
|
|
|
806
|
-
// ───
|
|
1101
|
+
// ─── Code Actions ──────────────────────────────────────
|
|
1102
|
+
|
|
1103
|
+
_onCodeAction(msg) {
|
|
1104
|
+
const { textDocument, range, context } = msg.params;
|
|
1105
|
+
const doc = this._documents.get(textDocument.uri);
|
|
1106
|
+
if (!doc) return this._respond(msg.id, []);
|
|
1107
|
+
|
|
1108
|
+
const actions = [];
|
|
1109
|
+
const diagnostics = context.diagnostics || [];
|
|
1110
|
+
|
|
1111
|
+
for (const diag of diagnostics) {
|
|
1112
|
+
const message = diag.message || '';
|
|
1113
|
+
|
|
1114
|
+
// Unused variable: offer "prefix with _"
|
|
1115
|
+
if (message.includes('declared but never used')) {
|
|
1116
|
+
const match = message.match(/'([^']+)'/);
|
|
1117
|
+
if (match) {
|
|
1118
|
+
const varName = match[1];
|
|
1119
|
+
const line = doc.text.split('\n')[diag.range.start.line] || '';
|
|
1120
|
+
const wordRegex = new RegExp(`\\b${varName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`);
|
|
1121
|
+
const wordMatch = wordRegex.exec(line);
|
|
1122
|
+
if (wordMatch) {
|
|
1123
|
+
actions.push({
|
|
1124
|
+
title: `Prefix '${varName}' with _`,
|
|
1125
|
+
kind: 'quickfix',
|
|
1126
|
+
diagnostics: [diag],
|
|
1127
|
+
edit: {
|
|
1128
|
+
changes: {
|
|
1129
|
+
[textDocument.uri]: [{
|
|
1130
|
+
range: {
|
|
1131
|
+
start: { line: diag.range.start.line, character: wordMatch.index },
|
|
1132
|
+
end: { line: diag.range.start.line, character: wordMatch.index + varName.length },
|
|
1133
|
+
},
|
|
1134
|
+
newText: `_${varName}`,
|
|
1135
|
+
}],
|
|
1136
|
+
},
|
|
1137
|
+
},
|
|
1138
|
+
});
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
// "Did you mean?" suggestion: offer replacement
|
|
1144
|
+
if (message.includes('is not defined') && message.includes('hint: did you mean')) {
|
|
1145
|
+
const nameMatch = message.match(/'([^']+)' is not defined/);
|
|
1146
|
+
const suggMatch = message.match(/did you mean '([^']+)'/);
|
|
1147
|
+
if (nameMatch && suggMatch) {
|
|
1148
|
+
const oldName = nameMatch[1];
|
|
1149
|
+
const newName = suggMatch[1];
|
|
1150
|
+
const line = doc.text.split('\n')[diag.range.start.line] || '';
|
|
1151
|
+
const wordRegex = new RegExp(`\\b${oldName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`);
|
|
1152
|
+
const wordMatch = wordRegex.exec(line);
|
|
1153
|
+
if (wordMatch) {
|
|
1154
|
+
actions.push({
|
|
1155
|
+
title: `Replace '${oldName}' with '${newName}'`,
|
|
1156
|
+
kind: 'quickfix',
|
|
1157
|
+
isPreferred: true,
|
|
1158
|
+
diagnostics: [diag],
|
|
1159
|
+
edit: {
|
|
1160
|
+
changes: {
|
|
1161
|
+
[textDocument.uri]: [{
|
|
1162
|
+
range: {
|
|
1163
|
+
start: { line: diag.range.start.line, character: wordMatch.index },
|
|
1164
|
+
end: { line: diag.range.start.line, character: wordMatch.index + oldName.length },
|
|
1165
|
+
},
|
|
1166
|
+
newText: newName,
|
|
1167
|
+
}],
|
|
1168
|
+
},
|
|
1169
|
+
},
|
|
1170
|
+
});
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
// Naming convention: offer rename
|
|
1176
|
+
if (message.includes('should use snake_case') || message.includes('should use PascalCase')) {
|
|
1177
|
+
const nameMatch = message.match(/'([^']+)'/);
|
|
1178
|
+
const hintMatch = (diag.message || '').match(/Rename '([^']+)' to '([^']+)'/);
|
|
1179
|
+
if (nameMatch && hintMatch) {
|
|
1180
|
+
const oldName = hintMatch[1];
|
|
1181
|
+
const newName = hintMatch[2];
|
|
1182
|
+
const line = doc.text.split('\n')[diag.range.start.line] || '';
|
|
1183
|
+
const wordRegex = new RegExp(`\\b${oldName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`);
|
|
1184
|
+
const wordMatch = wordRegex.exec(line);
|
|
1185
|
+
if (wordMatch) {
|
|
1186
|
+
actions.push({
|
|
1187
|
+
title: `Rename '${oldName}' to '${newName}'`,
|
|
1188
|
+
kind: 'quickfix',
|
|
1189
|
+
diagnostics: [diag],
|
|
1190
|
+
edit: {
|
|
1191
|
+
changes: {
|
|
1192
|
+
[textDocument.uri]: [{
|
|
1193
|
+
range: {
|
|
1194
|
+
start: { line: diag.range.start.line, character: wordMatch.index },
|
|
1195
|
+
end: { line: diag.range.start.line, character: wordMatch.index + oldName.length },
|
|
1196
|
+
},
|
|
1197
|
+
newText: newName,
|
|
1198
|
+
}],
|
|
1199
|
+
},
|
|
1200
|
+
},
|
|
1201
|
+
});
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
// Type mismatch with toString hint
|
|
1207
|
+
if (message.includes('Type mismatch') && message.includes('hint: try toString')) {
|
|
1208
|
+
actions.push({
|
|
1209
|
+
title: 'Wrap with toString()',
|
|
1210
|
+
kind: 'quickfix',
|
|
1211
|
+
diagnostics: [diag],
|
|
1212
|
+
});
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
// Type mismatch with Ok() hint
|
|
1216
|
+
if (message.includes('Type mismatch') && message.includes('hint: try Ok(value)')) {
|
|
1217
|
+
actions.push({
|
|
1218
|
+
title: 'Wrap with Ok()',
|
|
1219
|
+
kind: 'quickfix',
|
|
1220
|
+
diagnostics: [diag],
|
|
1221
|
+
});
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
// Type mismatch with Some() hint
|
|
1225
|
+
if (message.includes('Type mismatch') && message.includes('hint: try Some(value)')) {
|
|
1226
|
+
actions.push({
|
|
1227
|
+
title: 'Wrap with Some()',
|
|
1228
|
+
kind: 'quickfix',
|
|
1229
|
+
diagnostics: [diag],
|
|
1230
|
+
});
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
// Type mismatch: try toInt/toFloat/floor/round
|
|
1234
|
+
if (message.includes('Type mismatch') && message.includes('hint: try toInt')) {
|
|
1235
|
+
actions.push({
|
|
1236
|
+
title: 'Convert with toInt()',
|
|
1237
|
+
kind: 'quickfix',
|
|
1238
|
+
diagnostics: [diag],
|
|
1239
|
+
});
|
|
1240
|
+
}
|
|
1241
|
+
if (message.includes('Type mismatch') && message.includes('hint: try toFloat')) {
|
|
1242
|
+
actions.push({
|
|
1243
|
+
title: 'Convert with toFloat()',
|
|
1244
|
+
kind: 'quickfix',
|
|
1245
|
+
diagnostics: [diag],
|
|
1246
|
+
});
|
|
1247
|
+
}
|
|
1248
|
+
if (message.includes('hint: try floor(value)')) {
|
|
1249
|
+
actions.push({
|
|
1250
|
+
title: 'Convert with floor()',
|
|
1251
|
+
kind: 'quickfix',
|
|
1252
|
+
diagnostics: [diag],
|
|
1253
|
+
});
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
// Immutable variable: offer to change declaration to var
|
|
1257
|
+
if (message.includes('Cannot reassign immutable variable')) {
|
|
1258
|
+
const nameMatch = message.match(/variable '([^']+)'/);
|
|
1259
|
+
if (nameMatch) {
|
|
1260
|
+
const varName = nameMatch[1];
|
|
1261
|
+
// Find the line where the variable is first declared
|
|
1262
|
+
const docLines = doc.text.split('\n');
|
|
1263
|
+
for (let i = 0; i < docLines.length; i++) {
|
|
1264
|
+
const declMatch = docLines[i].match(new RegExp(`(?<![\\w])${varName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*=`));
|
|
1265
|
+
if (declMatch && i < diag.range.start.line) {
|
|
1266
|
+
const col = declMatch.index;
|
|
1267
|
+
actions.push({
|
|
1268
|
+
title: `Make '${varName}' mutable (add 'var')`,
|
|
1269
|
+
kind: 'quickfix',
|
|
1270
|
+
isPreferred: true,
|
|
1271
|
+
diagnostics: [diag],
|
|
1272
|
+
edit: {
|
|
1273
|
+
changes: {
|
|
1274
|
+
[textDocument.uri]: [{
|
|
1275
|
+
range: {
|
|
1276
|
+
start: { line: i, character: col },
|
|
1277
|
+
end: { line: i, character: col },
|
|
1278
|
+
},
|
|
1279
|
+
newText: 'var ',
|
|
1280
|
+
}],
|
|
1281
|
+
},
|
|
1282
|
+
},
|
|
1283
|
+
});
|
|
1284
|
+
break;
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
// Cannot use operator on immutable variable
|
|
1291
|
+
if (message.includes("Cannot use") && message.includes("on immutable variable")) {
|
|
1292
|
+
const nameMatch = message.match(/variable '([^']+)'/);
|
|
1293
|
+
if (nameMatch) {
|
|
1294
|
+
const varName = nameMatch[1];
|
|
1295
|
+
const docLines = doc.text.split('\n');
|
|
1296
|
+
for (let i = 0; i < docLines.length; i++) {
|
|
1297
|
+
const declMatch = docLines[i].match(new RegExp(`(?<![\\w])${varName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*=`));
|
|
1298
|
+
if (declMatch && i < diag.range.start.line) {
|
|
1299
|
+
const col = declMatch.index;
|
|
1300
|
+
actions.push({
|
|
1301
|
+
title: `Make '${varName}' mutable (add 'var')`,
|
|
1302
|
+
kind: 'quickfix',
|
|
1303
|
+
isPreferred: true,
|
|
1304
|
+
diagnostics: [diag],
|
|
1305
|
+
edit: {
|
|
1306
|
+
changes: {
|
|
1307
|
+
[textDocument.uri]: [{
|
|
1308
|
+
range: {
|
|
1309
|
+
start: { line: i, character: col },
|
|
1310
|
+
end: { line: i, character: col },
|
|
1311
|
+
},
|
|
1312
|
+
newText: 'var ',
|
|
1313
|
+
}],
|
|
1314
|
+
},
|
|
1315
|
+
},
|
|
1316
|
+
});
|
|
1317
|
+
break;
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
// await outside async: offer to add 'async' to the function
|
|
1324
|
+
if (message.includes("'await' can only be used inside an async function")) {
|
|
1325
|
+
const docLines = doc.text.split('\n');
|
|
1326
|
+
// Search backward for the enclosing 'fn' declaration
|
|
1327
|
+
for (let i = diag.range.start.line; i >= 0; i--) {
|
|
1328
|
+
const fnMatch = docLines[i].match(/(\s*)fn\s+/);
|
|
1329
|
+
if (fnMatch) {
|
|
1330
|
+
const col = fnMatch.index + fnMatch[1].length;
|
|
1331
|
+
actions.push({
|
|
1332
|
+
title: 'Add async to function',
|
|
1333
|
+
kind: 'quickfix',
|
|
1334
|
+
isPreferred: true,
|
|
1335
|
+
diagnostics: [diag],
|
|
1336
|
+
edit: {
|
|
1337
|
+
changes: {
|
|
1338
|
+
[textDocument.uri]: [{
|
|
1339
|
+
range: {
|
|
1340
|
+
start: { line: i, character: col },
|
|
1341
|
+
end: { line: i, character: col },
|
|
1342
|
+
},
|
|
1343
|
+
newText: 'async ',
|
|
1344
|
+
}],
|
|
1345
|
+
},
|
|
1346
|
+
},
|
|
1347
|
+
});
|
|
1348
|
+
break;
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
// Non-exhaustive match: offer to add wildcard arm
|
|
1354
|
+
if (message.includes('Non-exhaustive match')) {
|
|
1355
|
+
const variantMatch = message.match(/missing '([^']+)'/);
|
|
1356
|
+
if (variantMatch) {
|
|
1357
|
+
actions.push({
|
|
1358
|
+
title: `Add '_ => ...' catch-all arm`,
|
|
1359
|
+
kind: 'quickfix',
|
|
1360
|
+
diagnostics: [diag],
|
|
1361
|
+
});
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
// 'throw' is not a Tova keyword
|
|
1366
|
+
if (message.includes("'throw' is not a Tova keyword")) {
|
|
1367
|
+
const line = doc.text.split('\n')[diag.range.start.line] || '';
|
|
1368
|
+
const throwMatch = line.match(/\bthrow\b/);
|
|
1369
|
+
if (throwMatch) {
|
|
1370
|
+
actions.push({
|
|
1371
|
+
title: "Replace 'throw' with 'Err()'",
|
|
1372
|
+
kind: 'quickfix',
|
|
1373
|
+
isPreferred: true,
|
|
1374
|
+
diagnostics: [diag],
|
|
1375
|
+
edit: {
|
|
1376
|
+
changes: {
|
|
1377
|
+
[textDocument.uri]: [{
|
|
1378
|
+
range: {
|
|
1379
|
+
start: { line: diag.range.start.line, character: throwMatch.index },
|
|
1380
|
+
end: { line: diag.range.start.line, character: throwMatch.index + 5 },
|
|
1381
|
+
},
|
|
1382
|
+
newText: 'return Err(',
|
|
1383
|
+
}],
|
|
1384
|
+
},
|
|
1385
|
+
},
|
|
1386
|
+
});
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
// 'mut' not supported: offer 'var'
|
|
1391
|
+
if (message.includes("'mut' is not supported") || message.includes("Use 'var' for mutable")) {
|
|
1392
|
+
const line = doc.text.split('\n')[diag.range.start.line] || '';
|
|
1393
|
+
const mutMatch = line.match(/\bmut\b/);
|
|
1394
|
+
if (mutMatch) {
|
|
1395
|
+
actions.push({
|
|
1396
|
+
title: "Replace 'mut' with 'var'",
|
|
1397
|
+
kind: 'quickfix',
|
|
1398
|
+
isPreferred: true,
|
|
1399
|
+
diagnostics: [diag],
|
|
1400
|
+
edit: {
|
|
1401
|
+
changes: {
|
|
1402
|
+
[textDocument.uri]: [{
|
|
1403
|
+
range: {
|
|
1404
|
+
start: { line: diag.range.start.line, character: mutMatch.index },
|
|
1405
|
+
end: { line: diag.range.start.line, character: mutMatch.index + 3 },
|
|
1406
|
+
},
|
|
1407
|
+
newText: 'var',
|
|
1408
|
+
}],
|
|
1409
|
+
},
|
|
1410
|
+
},
|
|
1411
|
+
});
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
// Suppress unused variable with tova-ignore comment
|
|
1416
|
+
if (message.includes('declared but never used')) {
|
|
1417
|
+
const nameMatch = message.match(/'([^']+)'/);
|
|
1418
|
+
if (nameMatch) {
|
|
1419
|
+
const lineNum = diag.range.start.line;
|
|
1420
|
+
const docLines = doc.text.split('\n');
|
|
1421
|
+
const lineContent = docLines[lineNum] || '';
|
|
1422
|
+
const indent = lineContent.match(/^(\s*)/)[1];
|
|
1423
|
+
actions.push({
|
|
1424
|
+
title: `Suppress with // tova-ignore W001`,
|
|
1425
|
+
kind: 'quickfix',
|
|
1426
|
+
diagnostics: [diag],
|
|
1427
|
+
edit: {
|
|
1428
|
+
changes: {
|
|
1429
|
+
[textDocument.uri]: [{
|
|
1430
|
+
range: {
|
|
1431
|
+
start: { line: lineNum, character: 0 },
|
|
1432
|
+
end: { line: lineNum, character: 0 },
|
|
1433
|
+
},
|
|
1434
|
+
newText: `${indent}// tova-ignore W001\n`,
|
|
1435
|
+
}],
|
|
1436
|
+
},
|
|
1437
|
+
},
|
|
1438
|
+
});
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
this._respond(msg.id, actions);
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
// ─── Rename (scope-aware) ────────────────────────────────
|
|
807
1447
|
|
|
808
1448
|
_onRename(msg) {
|
|
809
1449
|
const { position, textDocument, newName } = msg.params;
|
|
@@ -814,10 +1454,88 @@ class TovaLanguageServer {
|
|
|
814
1454
|
const oldName = this._getWordAt(line, position.character);
|
|
815
1455
|
if (!oldName) return this._respond(msg.id, null);
|
|
816
1456
|
|
|
817
|
-
|
|
1457
|
+
const cached = this._diagnosticsCache.get(textDocument.uri);
|
|
1458
|
+
if (!cached || !cached.analyzer || !cached.analyzer.globalScope) {
|
|
1459
|
+
// Fallback to naive rename if no scope info
|
|
1460
|
+
return this._naiveRename(msg.id, textDocument.uri, doc.text, oldName, newName);
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
// Find which scope defines the binding the cursor is on
|
|
1464
|
+
const cursorLine = position.line + 1; // LSP 0-based to 1-based
|
|
1465
|
+
const cursorCol = position.character + 1;
|
|
1466
|
+
const cursorScope = cached.analyzer.globalScope.findScopeAtPosition(cursorLine, cursorCol);
|
|
1467
|
+
if (!cursorScope) {
|
|
1468
|
+
return this._naiveRename(msg.id, textDocument.uri, doc.text, oldName, newName);
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
// Walk up from cursor scope to find where this name is defined
|
|
1472
|
+
let definingScope = null;
|
|
1473
|
+
let scope = cursorScope;
|
|
1474
|
+
while (scope) {
|
|
1475
|
+
if (scope.symbols && scope.symbols.has(oldName)) {
|
|
1476
|
+
definingScope = scope;
|
|
1477
|
+
break;
|
|
1478
|
+
}
|
|
1479
|
+
scope = scope.parent;
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
if (!definingScope) {
|
|
1483
|
+
return this._naiveRename(msg.id, textDocument.uri, doc.text, oldName, newName);
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
// Collect edits: for each occurrence, check if it resolves to the same defining scope
|
|
818
1487
|
const edits = [];
|
|
819
1488
|
const docLines = doc.text.split('\n');
|
|
820
|
-
const
|
|
1489
|
+
const escaped = oldName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
1490
|
+
const wordRegex = new RegExp('\\b' + escaped + '\\b', 'g');
|
|
1491
|
+
|
|
1492
|
+
for (let i = 0; i < docLines.length; i++) {
|
|
1493
|
+
let match;
|
|
1494
|
+
while ((match = wordRegex.exec(docLines[i])) !== null) {
|
|
1495
|
+
const matchLine = i + 1; // 1-based
|
|
1496
|
+
const matchCol = match.index + 1;
|
|
1497
|
+
// Find the narrowest scope at this location
|
|
1498
|
+
const matchScope = cached.analyzer.globalScope.findScopeAtPosition(matchLine, matchCol);
|
|
1499
|
+
if (!matchScope) continue;
|
|
1500
|
+
|
|
1501
|
+
// Walk up from matchScope to see if the name resolves to definingScope
|
|
1502
|
+
let resolvedScope = null;
|
|
1503
|
+
let s = matchScope;
|
|
1504
|
+
while (s) {
|
|
1505
|
+
if (s.symbols && s.symbols.has(oldName)) {
|
|
1506
|
+
resolvedScope = s;
|
|
1507
|
+
break;
|
|
1508
|
+
}
|
|
1509
|
+
s = s.parent;
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
if (resolvedScope === definingScope) {
|
|
1513
|
+
edits.push({
|
|
1514
|
+
range: {
|
|
1515
|
+
start: { line: i, character: match.index },
|
|
1516
|
+
end: { line: i, character: match.index + oldName.length },
|
|
1517
|
+
},
|
|
1518
|
+
newText: newName,
|
|
1519
|
+
});
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
// If scope-aware rename found nothing (e.g. positional info gaps), fall back
|
|
1525
|
+
if (edits.length === 0) {
|
|
1526
|
+
return this._naiveRename(msg.id, textDocument.uri, doc.text, oldName, newName);
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
this._respond(msg.id, {
|
|
1530
|
+
changes: { [textDocument.uri]: edits },
|
|
1531
|
+
});
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
_naiveRename(id, uri, text, oldName, newName) {
|
|
1535
|
+
const edits = [];
|
|
1536
|
+
const docLines = text.split('\n');
|
|
1537
|
+
const escaped = oldName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
1538
|
+
const wordRegex = new RegExp('\\b' + escaped + '\\b', 'g');
|
|
821
1539
|
|
|
822
1540
|
for (let i = 0; i < docLines.length; i++) {
|
|
823
1541
|
let match;
|
|
@@ -832,8 +1550,8 @@ class TovaLanguageServer {
|
|
|
832
1550
|
}
|
|
833
1551
|
}
|
|
834
1552
|
|
|
835
|
-
this._respond(
|
|
836
|
-
changes: { [
|
|
1553
|
+
this._respond(id, {
|
|
1554
|
+
changes: { [uri]: edits },
|
|
837
1555
|
});
|
|
838
1556
|
}
|
|
839
1557
|
|
|
@@ -897,15 +1615,179 @@ class TovaLanguageServer {
|
|
|
897
1615
|
this._respond(msg.id, results.slice(0, 100));
|
|
898
1616
|
}
|
|
899
1617
|
|
|
1618
|
+
// ─── Inlay Hints ─────────────────────────────────────────
|
|
1619
|
+
|
|
1620
|
+
_onInlayHint(msg) {
|
|
1621
|
+
const { textDocument, range } = msg.params;
|
|
1622
|
+
const cached = this._diagnosticsCache.get(textDocument.uri);
|
|
1623
|
+
const doc = this._documents.get(textDocument.uri);
|
|
1624
|
+
if (!cached || !cached.analyzer || !doc) return this._respond(msg.id, []);
|
|
1625
|
+
|
|
1626
|
+
const hints = [];
|
|
1627
|
+
const docLines = doc.text.split('\n');
|
|
1628
|
+
const startLine = range.start.line;
|
|
1629
|
+
const endLine = range.end.line;
|
|
1630
|
+
|
|
1631
|
+
// ─── Type hints for variable bindings ─────────────
|
|
1632
|
+
// Match: `name = expr` and `var name = expr` (but NOT `name: Type = expr` which already has annotation)
|
|
1633
|
+
const bindingRegex = /^(\s*)(?:var\s+)?([a-zA-Z_]\w*)\s*=\s*(.+)/;
|
|
1634
|
+
const hasAnnotation = /^(\s*)(?:var\s+)?[a-zA-Z_]\w*\s*:\s*\w/;
|
|
1635
|
+
|
|
1636
|
+
for (let i = startLine; i <= endLine && i < docLines.length; i++) {
|
|
1637
|
+
const line = docLines[i];
|
|
1638
|
+
|
|
1639
|
+
// Skip lines that already have type annotations
|
|
1640
|
+
if (hasAnnotation.test(line)) continue;
|
|
1641
|
+
|
|
1642
|
+
const bindMatch = bindingRegex.exec(line);
|
|
1643
|
+
if (bindMatch) {
|
|
1644
|
+
const varName = bindMatch[2];
|
|
1645
|
+
// Skip private/special names
|
|
1646
|
+
if (varName.startsWith('_') || varName === '_') continue;
|
|
1647
|
+
// Skip keywords that look like assignments
|
|
1648
|
+
if (['fn', 'if', 'for', 'while', 'match', 'type', 'import', 'return', 'let'].includes(varName)) continue;
|
|
1649
|
+
|
|
1650
|
+
// Look up the symbol's inferred type
|
|
1651
|
+
const sym = this._findSymbolAtPosition(cached.analyzer, varName, { line: i, character: bindMatch[1].length + (line.includes('var ') ? 4 : 0) })
|
|
1652
|
+
|| this._findSymbolInScopes(cached.analyzer, varName);
|
|
1653
|
+
if (sym) {
|
|
1654
|
+
let typeStr = null;
|
|
1655
|
+
if (sym.inferredType) {
|
|
1656
|
+
typeStr = sym.inferredType;
|
|
1657
|
+
} else if (sym.typeAnnotation) {
|
|
1658
|
+
typeStr = sym.typeAnnotation;
|
|
1659
|
+
} else if (sym.type && typeof sym.type === 'object' && sym.type.name) {
|
|
1660
|
+
typeStr = sym.type.name;
|
|
1661
|
+
} else if (sym.kind === 'function') {
|
|
1662
|
+
typeStr = 'Function';
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
if (typeStr && typeStr !== 'Unknown' && typeStr !== 'Any') {
|
|
1666
|
+
// Position: right after the variable name
|
|
1667
|
+
const nameEnd = line.indexOf(varName) + varName.length;
|
|
1668
|
+
hints.push({
|
|
1669
|
+
position: { line: i, character: nameEnd },
|
|
1670
|
+
label: `: ${typeStr}`,
|
|
1671
|
+
kind: 1, // Type
|
|
1672
|
+
paddingLeft: false,
|
|
1673
|
+
paddingRight: true,
|
|
1674
|
+
});
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
// ─── Parameter name hints at call sites ─────────
|
|
1680
|
+
// Match function calls: name(arg1, arg2, ...)
|
|
1681
|
+
const callRegex = /\b([a-zA-Z_]\w*)\s*\(/g;
|
|
1682
|
+
let callMatch;
|
|
1683
|
+
while ((callMatch = callRegex.exec(line)) !== null) {
|
|
1684
|
+
const funcName = callMatch[1];
|
|
1685
|
+
// Skip keywords that look like function calls
|
|
1686
|
+
if (['if', 'for', 'while', 'match', 'fn', 'catch', 'switch'].includes(funcName)) continue;
|
|
1687
|
+
|
|
1688
|
+
// Look up function signature
|
|
1689
|
+
const funcSym = this._findSymbolInScopes(cached.analyzer, funcName);
|
|
1690
|
+
const params = funcSym?._params;
|
|
1691
|
+
if (!params || params.length === 0) continue;
|
|
1692
|
+
|
|
1693
|
+
// Parse the arguments (simplified — handles nested parens but not all edge cases)
|
|
1694
|
+
const argsStart = callMatch.index + callMatch[0].length;
|
|
1695
|
+
const argPositions = this._parseCallArgPositions(line, argsStart);
|
|
1696
|
+
|
|
1697
|
+
for (let ai = 0; ai < argPositions.length && ai < params.length; ai++) {
|
|
1698
|
+
const argPos = argPositions[ai];
|
|
1699
|
+
const argText = line.slice(argPos.start, argPos.end).trim();
|
|
1700
|
+
// Don't show hint if the argument is already the parameter name
|
|
1701
|
+
if (argText === params[ai]) continue;
|
|
1702
|
+
// Don't show hints for single-argument calls with obvious context
|
|
1703
|
+
if (params.length === 1 && argText.length <= 3) continue;
|
|
1704
|
+
// Don't show for self parameter
|
|
1705
|
+
if (params[ai] === 'self') continue;
|
|
1706
|
+
|
|
1707
|
+
hints.push({
|
|
1708
|
+
position: { line: i, character: argPos.start },
|
|
1709
|
+
label: `${params[ai]}:`,
|
|
1710
|
+
kind: 2, // Parameter
|
|
1711
|
+
paddingLeft: false,
|
|
1712
|
+
paddingRight: true,
|
|
1713
|
+
});
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
this._respond(msg.id, hints);
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
/**
|
|
1722
|
+
* Parse positions of arguments in a function call, handling nested parens.
|
|
1723
|
+
* Starts just after the opening '(' character.
|
|
1724
|
+
*/
|
|
1725
|
+
_parseCallArgPositions(line, startIdx) {
|
|
1726
|
+
const positions = [];
|
|
1727
|
+
let depth = 0;
|
|
1728
|
+
let argStart = startIdx;
|
|
1729
|
+
let inStr = null;
|
|
1730
|
+
|
|
1731
|
+
for (let i = startIdx; i < line.length; i++) {
|
|
1732
|
+
const ch = line[i];
|
|
1733
|
+
if (inStr) {
|
|
1734
|
+
if (ch === '\\') { i++; continue; }
|
|
1735
|
+
if (ch === inStr) inStr = null;
|
|
1736
|
+
continue;
|
|
1737
|
+
}
|
|
1738
|
+
if (ch === '"' || ch === "'" || ch === '`') { inStr = ch; continue; }
|
|
1739
|
+
if (ch === '(' || ch === '[' || ch === '{') { depth++; continue; }
|
|
1740
|
+
if (ch === ')' || ch === ']' || ch === '}') {
|
|
1741
|
+
if (depth === 0) {
|
|
1742
|
+
// End of argument list
|
|
1743
|
+
if (i > argStart) {
|
|
1744
|
+
positions.push({ start: argStart, end: i });
|
|
1745
|
+
}
|
|
1746
|
+
break;
|
|
1747
|
+
}
|
|
1748
|
+
depth--;
|
|
1749
|
+
continue;
|
|
1750
|
+
}
|
|
1751
|
+
if (ch === ',' && depth === 0) {
|
|
1752
|
+
positions.push({ start: argStart, end: i });
|
|
1753
|
+
argStart = i + 1;
|
|
1754
|
+
// Skip whitespace after comma
|
|
1755
|
+
while (argStart < line.length && line[argStart] === ' ') argStart++;
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1759
|
+
return positions;
|
|
1760
|
+
}
|
|
1761
|
+
|
|
900
1762
|
// ─── Utilities ────────────────────────────────────────────
|
|
901
1763
|
|
|
902
1764
|
_uriToPath(uri) {
|
|
903
1765
|
if (uri.startsWith('file://')) {
|
|
904
|
-
|
|
1766
|
+
let path = decodeURIComponent(uri.slice(7));
|
|
1767
|
+
// On Windows, file:///C:/path becomes /C:/path — strip leading slash
|
|
1768
|
+
if (/^\/[a-zA-Z]:/.test(path)) {
|
|
1769
|
+
path = path.slice(1);
|
|
1770
|
+
}
|
|
1771
|
+
return path;
|
|
905
1772
|
}
|
|
906
1773
|
return uri;
|
|
907
1774
|
}
|
|
908
1775
|
|
|
1776
|
+
_getBuiltinDetail(name) {
|
|
1777
|
+
const src = BUILTIN_FUNCTIONS[name];
|
|
1778
|
+
if (!src) return 'Tova built-in';
|
|
1779
|
+
// Constants (const PI = ...)
|
|
1780
|
+
const constMatch = src.match(/^const\s+\w+\s*=/);
|
|
1781
|
+
if (constMatch) return 'const';
|
|
1782
|
+
// Functions — extract params from source
|
|
1783
|
+
const fnMatch = src.match(/^(?:async\s+)?function\s+\w+\s*\(([^)]*)\)/);
|
|
1784
|
+
if (fnMatch) {
|
|
1785
|
+
const params = fnMatch[1].trim();
|
|
1786
|
+
return params ? `fn(${params})` : 'fn()';
|
|
1787
|
+
}
|
|
1788
|
+
return 'Tova built-in';
|
|
1789
|
+
}
|
|
1790
|
+
|
|
909
1791
|
_getWordAt(line, character) {
|
|
910
1792
|
let start = character;
|
|
911
1793
|
let end = character;
|