tova 0.3.4 → 0.3.6
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 +438 -58
- package/package.json +1 -1
- package/src/analyzer/analyzer.js +172 -32
- package/src/analyzer/client-analyzer.js +21 -5
- package/src/analyzer/scope.js +78 -3
- package/src/codegen/base-codegen.js +754 -45
- package/src/codegen/client-codegen.js +293 -36
- package/src/codegen/codegen.js +10 -15
- package/src/codegen/server-codegen.js +189 -40
- package/src/codegen/wasm-codegen.js +610 -0
- package/src/lexer/lexer.js +157 -109
- package/src/lexer/tokens.js +3 -0
- package/src/lsp/server.js +148 -12
- package/src/parser/ast.js +2 -1
- package/src/parser/client-parser.js +10 -3
- package/src/parser/parser.js +144 -150
- package/src/runtime/embedded.js +1 -1
- package/src/runtime/reactivity.js +307 -59
- package/src/runtime/ssr.js +101 -34
- package/src/stdlib/inline.js +333 -24
- package/src/stdlib/native-bridge.js +150 -0
- package/src/version.js +1 -1
package/src/lsp/server.js
CHANGED
|
@@ -145,7 +145,7 @@ class TovaLanguageServer {
|
|
|
145
145
|
capabilities: {
|
|
146
146
|
textDocumentSync: {
|
|
147
147
|
openClose: true,
|
|
148
|
-
change:
|
|
148
|
+
change: 2, // Incremental sync — receive only changed ranges
|
|
149
149
|
save: { includeText: true },
|
|
150
150
|
},
|
|
151
151
|
completionProvider: {
|
|
@@ -183,11 +183,56 @@ class TovaLanguageServer {
|
|
|
183
183
|
|
|
184
184
|
_onDidChange(params) {
|
|
185
185
|
const { uri, version } = params.textDocument;
|
|
186
|
-
const
|
|
187
|
-
if (
|
|
188
|
-
|
|
189
|
-
|
|
186
|
+
const changes = params.contentChanges;
|
|
187
|
+
if (!changes || changes.length === 0) return;
|
|
188
|
+
|
|
189
|
+
const doc = this._documents.get(uri);
|
|
190
|
+
if (!doc) return;
|
|
191
|
+
|
|
192
|
+
let text = doc.text;
|
|
193
|
+
|
|
194
|
+
for (const change of changes) {
|
|
195
|
+
if (change.range) {
|
|
196
|
+
// Incremental sync: apply range edit
|
|
197
|
+
text = this._applyEdit(text, change.range, change.text);
|
|
198
|
+
} else {
|
|
199
|
+
// Full sync fallback (e.g., if client sends full text)
|
|
200
|
+
text = change.text;
|
|
201
|
+
}
|
|
190
202
|
}
|
|
203
|
+
|
|
204
|
+
this._documents.set(uri, { text, version });
|
|
205
|
+
this._debouncedValidate(uri, text);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Apply a single incremental text edit to the document
|
|
209
|
+
_applyEdit(text, range, newText) {
|
|
210
|
+
const startOffset = this._positionToOffset(text, range.start);
|
|
211
|
+
const endOffset = this._positionToOffset(text, range.end);
|
|
212
|
+
return text.slice(0, startOffset) + newText + text.slice(endOffset);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Convert { line, character } position to byte offset in text
|
|
216
|
+
_positionToOffset(text, pos) {
|
|
217
|
+
let line = 0;
|
|
218
|
+
let offset = 0;
|
|
219
|
+
while (line < pos.line && offset < text.length) {
|
|
220
|
+
if (text[offset] === '\n') line++;
|
|
221
|
+
offset++;
|
|
222
|
+
}
|
|
223
|
+
return offset + pos.character;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
_debouncedValidate(uri, text) {
|
|
227
|
+
if (!this._validateTimers) this._validateTimers = new Map();
|
|
228
|
+
const existing = this._validateTimers.get(uri);
|
|
229
|
+
if (existing) clearTimeout(existing);
|
|
230
|
+
this._validateTimers.set(uri, setTimeout(() => {
|
|
231
|
+
this._validateTimers.delete(uri);
|
|
232
|
+
// Re-read latest text in case more changes arrived
|
|
233
|
+
const doc = this._documents.get(uri);
|
|
234
|
+
if (doc) this._validateDocument(uri, doc.text);
|
|
235
|
+
}, 200));
|
|
191
236
|
}
|
|
192
237
|
|
|
193
238
|
_onDidClose(params) {
|
|
@@ -222,6 +267,8 @@ class TovaLanguageServer {
|
|
|
222
267
|
// LSP always runs with strict: true for better diagnostics
|
|
223
268
|
const analyzer = new Analyzer(ast, filename, { strict: true });
|
|
224
269
|
const result = analyzer.analyze();
|
|
270
|
+
// Build sorted scope index for O(log n) position lookups
|
|
271
|
+
analyzer.globalScope.buildIndex();
|
|
225
272
|
const { warnings } = result;
|
|
226
273
|
const typeRegistry = TypeRegistry.fromAnalyzer(analyzer);
|
|
227
274
|
|
|
@@ -296,6 +343,7 @@ class TovaLanguageServer {
|
|
|
296
343
|
try {
|
|
297
344
|
const analyzer = new Analyzer(partialAST, filename, { tolerant: true, strict: true });
|
|
298
345
|
const result = analyzer.analyze();
|
|
346
|
+
analyzer.globalScope.buildIndex();
|
|
299
347
|
const typeRegistry = TypeRegistry.fromAnalyzer(analyzer);
|
|
300
348
|
this._diagnosticsCache.set(uri, { ast: partialAST, analyzer, text, typeRegistry });
|
|
301
349
|
for (const w of result.warnings) {
|
|
@@ -1555,7 +1603,7 @@ class TovaLanguageServer {
|
|
|
1555
1603
|
});
|
|
1556
1604
|
}
|
|
1557
1605
|
|
|
1558
|
-
// ─── References
|
|
1606
|
+
// ─── References (scope-aware) ───────────────────────────
|
|
1559
1607
|
|
|
1560
1608
|
_onReferences(msg) {
|
|
1561
1609
|
const { position, textDocument } = msg.params;
|
|
@@ -1566,15 +1614,90 @@ class TovaLanguageServer {
|
|
|
1566
1614
|
const word = this._getWordAt(line, position.character);
|
|
1567
1615
|
if (!word) return this._respond(msg.id, []);
|
|
1568
1616
|
|
|
1617
|
+
const cached = this._diagnosticsCache.get(textDocument.uri);
|
|
1618
|
+
if (!cached || !cached.analyzer || !cached.analyzer.globalScope) {
|
|
1619
|
+
// Fallback to naive text search if no scope info
|
|
1620
|
+
return this._naiveReferences(msg.id, textDocument.uri, doc.text, word);
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
// Find the scope at cursor and walk up to find the defining scope
|
|
1624
|
+
const cursorLine = position.line + 1;
|
|
1625
|
+
const cursorCol = position.character + 1;
|
|
1626
|
+
const cursorScope = cached.analyzer.globalScope.findScopeAtPosition(cursorLine, cursorCol);
|
|
1627
|
+
if (!cursorScope) {
|
|
1628
|
+
return this._naiveReferences(msg.id, textDocument.uri, doc.text, word);
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
let definingScope = null;
|
|
1632
|
+
let scope = cursorScope;
|
|
1633
|
+
while (scope) {
|
|
1634
|
+
if (scope.symbols && scope.symbols.has(word)) {
|
|
1635
|
+
definingScope = scope;
|
|
1636
|
+
break;
|
|
1637
|
+
}
|
|
1638
|
+
scope = scope.parent;
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
if (!definingScope) {
|
|
1642
|
+
return this._naiveReferences(msg.id, textDocument.uri, doc.text, word);
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
// Collect locations: for each text occurrence, check if it resolves to the same scope
|
|
1569
1646
|
const locations = [];
|
|
1570
1647
|
const docLines = doc.text.split('\n');
|
|
1571
|
-
const
|
|
1648
|
+
const escaped = word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
1649
|
+
const wordRegex = new RegExp('\\b' + escaped + '\\b', 'g');
|
|
1650
|
+
|
|
1651
|
+
for (let i = 0; i < docLines.length; i++) {
|
|
1652
|
+
let match;
|
|
1653
|
+
while ((match = wordRegex.exec(docLines[i])) !== null) {
|
|
1654
|
+
const matchLine = i + 1;
|
|
1655
|
+
const matchCol = match.index + 1;
|
|
1656
|
+
const matchScope = cached.analyzer.globalScope.findScopeAtPosition(matchLine, matchCol);
|
|
1657
|
+
if (!matchScope) continue;
|
|
1658
|
+
|
|
1659
|
+
// Walk up from matchScope to see if the name resolves to definingScope
|
|
1660
|
+
let resolvedScope = null;
|
|
1661
|
+
let s = matchScope;
|
|
1662
|
+
while (s) {
|
|
1663
|
+
if (s.symbols && s.symbols.has(word)) {
|
|
1664
|
+
resolvedScope = s;
|
|
1665
|
+
break;
|
|
1666
|
+
}
|
|
1667
|
+
s = s.parent;
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
if (resolvedScope === definingScope) {
|
|
1671
|
+
locations.push({
|
|
1672
|
+
uri: textDocument.uri,
|
|
1673
|
+
range: {
|
|
1674
|
+
start: { line: i, character: match.index },
|
|
1675
|
+
end: { line: i, character: match.index + word.length },
|
|
1676
|
+
},
|
|
1677
|
+
});
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
// Fallback if scope-aware search found nothing (e.g. positional info gaps)
|
|
1683
|
+
if (locations.length === 0) {
|
|
1684
|
+
return this._naiveReferences(msg.id, textDocument.uri, doc.text, word);
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
this._respond(msg.id, locations);
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
_naiveReferences(id, uri, text, word) {
|
|
1691
|
+
const locations = [];
|
|
1692
|
+
const docLines = text.split('\n');
|
|
1693
|
+
const escaped = word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
1694
|
+
const wordRegex = new RegExp('\\b' + escaped + '\\b', 'g');
|
|
1572
1695
|
|
|
1573
1696
|
for (let i = 0; i < docLines.length; i++) {
|
|
1574
1697
|
let match;
|
|
1575
1698
|
while ((match = wordRegex.exec(docLines[i])) !== null) {
|
|
1576
1699
|
locations.push({
|
|
1577
|
-
uri:
|
|
1700
|
+
uri: uri,
|
|
1578
1701
|
range: {
|
|
1579
1702
|
start: { line: i, character: match.index },
|
|
1580
1703
|
end: { line: i, character: match.index + word.length },
|
|
@@ -1583,7 +1706,7 @@ class TovaLanguageServer {
|
|
|
1583
1706
|
}
|
|
1584
1707
|
}
|
|
1585
1708
|
|
|
1586
|
-
this._respond(
|
|
1709
|
+
this._respond(id, locations);
|
|
1587
1710
|
}
|
|
1588
1711
|
|
|
1589
1712
|
// ─── Workspace Symbol ──────────────────────────────────
|
|
@@ -1869,6 +1992,19 @@ class TovaLanguageServer {
|
|
|
1869
1992
|
}
|
|
1870
1993
|
}
|
|
1871
1994
|
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1995
|
+
export { TovaLanguageServer };
|
|
1996
|
+
|
|
1997
|
+
// Auto-start when imported (for `tova lsp` command).
|
|
1998
|
+
// Use startServer() export for explicit control.
|
|
1999
|
+
export function startServer() {
|
|
2000
|
+
const server = new TovaLanguageServer();
|
|
2001
|
+
server.start();
|
|
2002
|
+
return server;
|
|
2003
|
+
}
|
|
2004
|
+
|
|
2005
|
+
// Start automatically — callers that only want the class import should
|
|
2006
|
+
// use dynamic import with test-aware filtering or call startServer() explicitly
|
|
2007
|
+
const _autoStart = globalThis.__TOVA_LSP_NO_AUTOSTART !== true;
|
|
2008
|
+
if (_autoStart) {
|
|
2009
|
+
startServer();
|
|
2010
|
+
}
|
package/src/parser/ast.js
CHANGED
|
@@ -77,7 +77,7 @@ export class LetDestructure {
|
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
export class FunctionDeclaration {
|
|
80
|
-
constructor(name, params, body, returnType, loc, isAsync = false, typeParams = []) {
|
|
80
|
+
constructor(name, params, body, returnType, loc, isAsync = false, typeParams = [], decorators = []) {
|
|
81
81
|
this.type = 'FunctionDeclaration';
|
|
82
82
|
this.name = name;
|
|
83
83
|
this.typeParams = typeParams; // Array of type parameter names (generics)
|
|
@@ -85,6 +85,7 @@ export class FunctionDeclaration {
|
|
|
85
85
|
this.body = body; // BlockStatement or Expression (implicit return)
|
|
86
86
|
this.returnType = returnType; // optional type annotation
|
|
87
87
|
this.isAsync = isAsync;
|
|
88
|
+
this.decorators = decorators; // Array of { name, args } for @decorator syntax
|
|
88
89
|
this.loc = loc;
|
|
89
90
|
}
|
|
90
91
|
}
|
|
@@ -293,6 +293,13 @@ export function installClientParser(ParserClass) {
|
|
|
293
293
|
suffix = this.expect(TokenType.IDENTIFIER, "Expected name after ':'").value;
|
|
294
294
|
}
|
|
295
295
|
name = `${name}:${suffix}`;
|
|
296
|
+
// Consume event modifiers: on:click.stop.prevent
|
|
297
|
+
if (name.startsWith('on:') && this.check(TokenType.DOT)) {
|
|
298
|
+
while (this.match(TokenType.DOT)) {
|
|
299
|
+
const mod = this.expect(TokenType.IDENTIFIER, "Expected modifier name after '.'").value;
|
|
300
|
+
name += `.${mod}`;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
296
303
|
}
|
|
297
304
|
|
|
298
305
|
if (!this.match(TokenType.ASSIGN)) {
|
|
@@ -402,17 +409,17 @@ export function installClientParser(ParserClass) {
|
|
|
402
409
|
if (!this.match(TokenType.COMMA)) break;
|
|
403
410
|
}
|
|
404
411
|
this.expect(TokenType.RBRACKET, "Expected ']' in destructuring pattern");
|
|
405
|
-
variable =
|
|
412
|
+
variable = new AST.ArrayPattern(elements, l);
|
|
406
413
|
} else if (this.check(TokenType.LBRACE)) {
|
|
407
414
|
// Object destructuring: {name, age}
|
|
408
415
|
this.advance(); // consume {
|
|
409
416
|
const props = [];
|
|
410
417
|
while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
|
|
411
|
-
props.push(this.expect(TokenType.IDENTIFIER, "Expected property name in object pattern").value);
|
|
418
|
+
props.push({ key: this.expect(TokenType.IDENTIFIER, "Expected property name in object pattern").value });
|
|
412
419
|
if (!this.match(TokenType.COMMA)) break;
|
|
413
420
|
}
|
|
414
421
|
this.expect(TokenType.RBRACE, "Expected '}' in destructuring pattern");
|
|
415
|
-
variable =
|
|
422
|
+
variable = new AST.ObjectPattern(props, l);
|
|
416
423
|
} else {
|
|
417
424
|
variable = this.expect(TokenType.IDENTIFIER, "Expected loop variable").value;
|
|
418
425
|
}
|