tova 0.3.0 → 0.3.2
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 +1401 -111
- package/package.json +4 -7
- package/src/analyzer/analyzer.js +831 -709
- 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 +467 -109
- package/src/codegen/client-codegen.js +92 -42
- package/src/codegen/codegen.js +65 -5
- package/src/codegen/server-codegen.js +290 -36
- 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 +305 -63
- package/src/lexer/tokens.js +19 -0
- package/src/lsp/server.js +892 -30
- 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 +491 -1064
- 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 +191 -10
- package/src/stdlib/advanced-collections.js +81 -0
- package/src/stdlib/inline.js +549 -6
- package/src/version.js +1 -1
package/src/lsp/server.js
CHANGED
|
@@ -8,6 +8,7 @@ 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
|
|
@@ -117,7 +118,9 @@ class TovaLanguageServer {
|
|
|
117
118
|
case 'textDocument/signatureHelp': return this._onSignatureHelp(msg);
|
|
118
119
|
case 'textDocument/formatting': return this._onFormatting(msg);
|
|
119
120
|
case 'textDocument/rename': return this._onRename(msg);
|
|
121
|
+
case 'textDocument/codeAction': return this._onCodeAction(msg);
|
|
120
122
|
case 'textDocument/references': return this._onReferences(msg);
|
|
123
|
+
case 'textDocument/inlayHint': return this._onInlayHint(msg);
|
|
121
124
|
case 'workspace/symbol': return this._onWorkspaceSymbol(msg);
|
|
122
125
|
default: return this._respondError(msg.id, -32601, `Method not found: ${method}`);
|
|
123
126
|
}
|
|
@@ -154,9 +157,13 @@ class TovaLanguageServer {
|
|
|
154
157
|
signatureHelpProvider: {
|
|
155
158
|
triggerCharacters: ['(', ','],
|
|
156
159
|
},
|
|
160
|
+
codeActionProvider: {
|
|
161
|
+
codeActionKinds: ['quickfix'],
|
|
162
|
+
},
|
|
157
163
|
documentFormattingProvider: true,
|
|
158
164
|
renameProvider: { prepareProvider: false },
|
|
159
165
|
referencesProvider: true,
|
|
166
|
+
inlayHintProvider: true,
|
|
160
167
|
workspaceSymbolProvider: true,
|
|
161
168
|
},
|
|
162
169
|
});
|
|
@@ -242,8 +249,9 @@ class TovaLanguageServer {
|
|
|
242
249
|
},
|
|
243
250
|
severity,
|
|
244
251
|
source: 'tova',
|
|
245
|
-
message: w.message,
|
|
252
|
+
message: w.hint ? `${w.message} (hint: ${w.hint})` : w.message,
|
|
246
253
|
};
|
|
254
|
+
if (w.code) diag.code = w.code;
|
|
247
255
|
// Add unnecessary tag for unused variables
|
|
248
256
|
if (w.message.includes('declared but never used')) {
|
|
249
257
|
diag.tags = [1]; // Unnecessary
|
|
@@ -333,6 +341,8 @@ class TovaLanguageServer {
|
|
|
333
341
|
if (msg.includes('Non-exhaustive match')) return 2;
|
|
334
342
|
// Type mismatches in strict mode → Error (1)
|
|
335
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;
|
|
336
346
|
// Default → Warning (2)
|
|
337
347
|
return 2;
|
|
338
348
|
}
|
|
@@ -415,7 +425,7 @@ class TovaLanguageServer {
|
|
|
415
425
|
|
|
416
426
|
// Keywords
|
|
417
427
|
const keywords = [
|
|
418
|
-
'fn', 'let', 'if', 'elif', 'else', 'for', 'while', 'in',
|
|
428
|
+
'fn', 'let', 'if', 'elif', 'else', 'for', 'while', 'loop', 'when', 'in',
|
|
419
429
|
'return', 'match', 'type', 'import', 'from', 'true', 'false',
|
|
420
430
|
'nil', 'server', 'client', 'shared', 'pub', 'mut',
|
|
421
431
|
'try', 'catch', 'finally', 'break', 'continue', 'async', 'await',
|
|
@@ -427,15 +437,17 @@ class TovaLanguageServer {
|
|
|
427
437
|
}
|
|
428
438
|
}
|
|
429
439
|
|
|
430
|
-
// Built-in functions
|
|
431
|
-
const
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
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' });
|
|
439
451
|
}
|
|
440
452
|
}
|
|
441
453
|
|
|
@@ -444,7 +456,7 @@ class TovaLanguageServer {
|
|
|
444
456
|
if (cached?.analyzer) {
|
|
445
457
|
const symbols = this._collectSymbols(cached.analyzer);
|
|
446
458
|
for (const sym of symbols) {
|
|
447
|
-
if (sym.name.startsWith(prefix) && !
|
|
459
|
+
if (sym.name.startsWith(prefix) && !BUILTIN_NAMES.has(sym.name)) {
|
|
448
460
|
items.push({
|
|
449
461
|
label: sym.name,
|
|
450
462
|
kind: sym.kind === 'function' ? 3 : sym.kind === 'type' ? 22 : 6,
|
|
@@ -618,23 +630,291 @@ class TovaLanguageServer {
|
|
|
618
630
|
const word = this._getWordAt(line, position.character);
|
|
619
631
|
if (!word) return this._respond(msg.id, null);
|
|
620
632
|
|
|
621
|
-
// Check builtins
|
|
633
|
+
// Check builtins — comprehensive hover docs for all stdlib functions
|
|
622
634
|
const builtinDocs = {
|
|
635
|
+
// Core
|
|
623
636
|
'print': '`fn print(...args)` — Print values to console',
|
|
624
637
|
'len': '`fn len(v)` — Get length of string, array, or object',
|
|
625
|
-
'range': '`fn range(start, end
|
|
638
|
+
'range': '`fn range(start, end?, step?)` — Generate array of numbers',
|
|
626
639
|
'enumerate': '`fn enumerate(arr)` — Returns [[index, value], ...]',
|
|
627
|
-
'
|
|
628
|
-
|
|
629
|
-
'
|
|
630
|
-
'
|
|
631
|
-
'
|
|
632
|
-
'
|
|
633
|
-
|
|
634
|
-
'
|
|
635
|
-
'
|
|
636
|
-
'
|
|
637
|
-
'
|
|
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',
|
|
638
918
|
};
|
|
639
919
|
|
|
640
920
|
if (builtinDocs[word]) {
|
|
@@ -818,7 +1098,352 @@ class TovaLanguageServer {
|
|
|
818
1098
|
}
|
|
819
1099
|
}
|
|
820
1100
|
|
|
821
|
-
// ───
|
|
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) ────────────────────────────────
|
|
822
1447
|
|
|
823
1448
|
_onRename(msg) {
|
|
824
1449
|
const { position, textDocument, newName } = msg.params;
|
|
@@ -829,10 +1454,88 @@ class TovaLanguageServer {
|
|
|
829
1454
|
const oldName = this._getWordAt(line, position.character);
|
|
830
1455
|
if (!oldName) return this._respond(msg.id, null);
|
|
831
1456
|
|
|
832
|
-
|
|
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
|
|
833
1487
|
const edits = [];
|
|
834
1488
|
const docLines = doc.text.split('\n');
|
|
835
|
-
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');
|
|
836
1539
|
|
|
837
1540
|
for (let i = 0; i < docLines.length; i++) {
|
|
838
1541
|
let match;
|
|
@@ -847,8 +1550,8 @@ class TovaLanguageServer {
|
|
|
847
1550
|
}
|
|
848
1551
|
}
|
|
849
1552
|
|
|
850
|
-
this._respond(
|
|
851
|
-
changes: { [
|
|
1553
|
+
this._respond(id, {
|
|
1554
|
+
changes: { [uri]: edits },
|
|
852
1555
|
});
|
|
853
1556
|
}
|
|
854
1557
|
|
|
@@ -912,6 +1615,150 @@ class TovaLanguageServer {
|
|
|
912
1615
|
this._respond(msg.id, results.slice(0, 100));
|
|
913
1616
|
}
|
|
914
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
|
+
|
|
915
1762
|
// ─── Utilities ────────────────────────────────────────────
|
|
916
1763
|
|
|
917
1764
|
_uriToPath(uri) {
|
|
@@ -926,6 +1773,21 @@ class TovaLanguageServer {
|
|
|
926
1773
|
return uri;
|
|
927
1774
|
}
|
|
928
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
|
+
|
|
929
1791
|
_getWordAt(line, character) {
|
|
930
1792
|
let start = character;
|
|
931
1793
|
let end = character;
|