naider 2.0.0 → 2.0.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/README.md +105 -29
- package/bin/naide.js +471 -25
- package/lsp/server.js +210 -100
- package/mcp/server.js +246 -21
- package/package.json +7 -2
- package/src/generator-c.js +34 -14
- package/src/generator-go.js +26 -0
- package/src/project-tools.js +785 -0
- package/src/typechecker.js +87 -5
- package/vscode-naide/README.md +1 -1
- package/vscode-naide/package-lock.json +2 -2
- package/vscode-naide/package.json +1 -1
package/lsp/server.js
CHANGED
|
@@ -1,24 +1,74 @@
|
|
|
1
1
|
import { compile } from '../src/index.js';
|
|
2
|
+
import { Lexer } from '../src/lexer.js';
|
|
2
3
|
|
|
3
4
|
const documents = new Map();
|
|
4
5
|
const symbolIndex = new Map();
|
|
5
|
-
let buffer =
|
|
6
|
+
let buffer = Buffer.alloc(0);
|
|
7
|
+
const MAX_HEADER_BYTES = 8 * 1024;
|
|
8
|
+
const MAX_MESSAGE_BYTES = 32 * 1024 * 1024;
|
|
6
9
|
|
|
7
|
-
process.stdin.setEncoding('utf-8');
|
|
8
10
|
process.stdin.on('data', (chunk) => {
|
|
9
|
-
buffer
|
|
11
|
+
buffer = Buffer.concat([buffer, chunk]);
|
|
10
12
|
while (true) {
|
|
11
|
-
const headerEnd = buffer.indexOf('\r\n\r\n');
|
|
12
|
-
if (headerEnd === -1)
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
const headerEnd = buffer.indexOf('\r\n\r\n', 0, 'ascii');
|
|
14
|
+
if (headerEnd === -1) {
|
|
15
|
+
if (buffer.length > MAX_HEADER_BYTES) {
|
|
16
|
+
console.error('NAIDE LSP: rejected an oversized message header');
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
break;
|
|
20
|
+
}
|
|
21
|
+
if (headerEnd > MAX_HEADER_BYTES) {
|
|
22
|
+
console.error('NAIDE LSP: rejected an oversized message header');
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
const header = buffer.toString('ascii', 0, headerEnd);
|
|
26
|
+
const lengthMatch = header.match(/^Content-Length:\s*(\d+)\s*$/im);
|
|
27
|
+
if (!lengthMatch) {
|
|
28
|
+
buffer = buffer.subarray(headerEnd + 4);
|
|
29
|
+
console.error('NAIDE LSP: discarded a message with no Content-Length header');
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
const contentLength = Number(lengthMatch[1]);
|
|
33
|
+
if (!Number.isSafeInteger(contentLength) || contentLength > MAX_MESSAGE_BYTES) {
|
|
34
|
+
console.error('NAIDE LSP: rejected an oversized or invalid message body');
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
17
37
|
const start = headerEnd + 4;
|
|
18
38
|
if (buffer.length < start + contentLength) break;
|
|
19
|
-
const content = buffer.
|
|
20
|
-
buffer = buffer.
|
|
21
|
-
|
|
39
|
+
const content = buffer.toString('utf8', start, start + contentLength);
|
|
40
|
+
buffer = buffer.subarray(start + contentLength);
|
|
41
|
+
|
|
42
|
+
let message;
|
|
43
|
+
try {
|
|
44
|
+
message = JSON.parse(content);
|
|
45
|
+
} catch (error) {
|
|
46
|
+
console.error(`NAIDE LSP: invalid JSON-RPC message: ${error.message}`);
|
|
47
|
+
send({ jsonrpc: '2.0', id: null, error: { code: -32700, message: 'Parse error' } });
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!isValidMessage(message)) {
|
|
52
|
+
const id = message && typeof message === 'object' && !Array.isArray(message)
|
|
53
|
+
&& (typeof message.id === 'string' || typeof message.id === 'number' || message.id === null)
|
|
54
|
+
? message.id
|
|
55
|
+
: null;
|
|
56
|
+
send({ jsonrpc: '2.0', id, error: { code: -32600, message: 'Invalid Request' } });
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
handleMessage(message);
|
|
62
|
+
} catch (error) {
|
|
63
|
+
console.error(`NAIDE LSP: failed to handle ${message.method}: ${error.stack || error}`);
|
|
64
|
+
if (message.id !== undefined) {
|
|
65
|
+
send({
|
|
66
|
+
jsonrpc: '2.0',
|
|
67
|
+
id: message.id,
|
|
68
|
+
error: { code: -32603, message: 'Internal error' },
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
22
72
|
}
|
|
23
73
|
});
|
|
24
74
|
|
|
@@ -30,6 +80,20 @@ function send(msg) {
|
|
|
30
80
|
function respond(id, result) { send({ jsonrpc: '2.0', id, result }); }
|
|
31
81
|
function notify(method, params) { send({ jsonrpc: '2.0', method, params }); }
|
|
32
82
|
|
|
83
|
+
function isValidMessage(message) {
|
|
84
|
+
return Boolean(message)
|
|
85
|
+
&& typeof message === 'object'
|
|
86
|
+
&& !Array.isArray(message)
|
|
87
|
+
&& message.jsonrpc === '2.0'
|
|
88
|
+
&& typeof message.method === 'string'
|
|
89
|
+
&& (message.id === undefined
|
|
90
|
+
|| message.id === null
|
|
91
|
+
|| typeof message.id === 'string'
|
|
92
|
+
|| (typeof message.id === 'number' && Number.isFinite(message.id)))
|
|
93
|
+
&& (message.params === undefined
|
|
94
|
+
|| (message.params !== null && typeof message.params === 'object'));
|
|
95
|
+
}
|
|
96
|
+
|
|
33
97
|
function handleMessage(msg) {
|
|
34
98
|
switch (msg.method) {
|
|
35
99
|
case 'initialize':
|
|
@@ -40,6 +104,7 @@ function handleMessage(msg) {
|
|
|
40
104
|
hoverProvider: true,
|
|
41
105
|
definitionProvider: true,
|
|
42
106
|
referencesProvider: true,
|
|
107
|
+
documentSymbolProvider: true,
|
|
43
108
|
}
|
|
44
109
|
});
|
|
45
110
|
break;
|
|
@@ -60,6 +125,7 @@ function handleMessage(msg) {
|
|
|
60
125
|
|
|
61
126
|
case 'textDocument/didClose':
|
|
62
127
|
documents.delete(msg.params.textDocument.uri);
|
|
128
|
+
symbolIndex.delete(msg.params.textDocument.uri);
|
|
63
129
|
notify('textDocument/publishDiagnostics', { uri: msg.params.textDocument.uri, diagnostics: [] });
|
|
64
130
|
break;
|
|
65
131
|
|
|
@@ -79,6 +145,25 @@ function handleMessage(msg) {
|
|
|
79
145
|
respond(msg.id, getReferences(msg.params));
|
|
80
146
|
break;
|
|
81
147
|
|
|
148
|
+
case 'textDocument/documentSymbol': {
|
|
149
|
+
const index = symbolIndex.get(msg.params.textDocument.uri);
|
|
150
|
+
const symbols = [];
|
|
151
|
+
if (index) {
|
|
152
|
+
for (const [name, definition] of index.definitions) {
|
|
153
|
+
const symbolKind = definition.kind === 'function' ? 12
|
|
154
|
+
: definition.kind === 'class' ? 5
|
|
155
|
+
: definition.kind === 'schema' ? 23 : 13;
|
|
156
|
+
const range = {
|
|
157
|
+
start: { line: definition.line, character: definition.col },
|
|
158
|
+
end: { line: definition.line, character: definition.col + name.length },
|
|
159
|
+
};
|
|
160
|
+
symbols.push({ name, kind: symbolKind, range, selectionRange: range });
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
respond(msg.id, symbols);
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
|
|
82
167
|
case 'shutdown':
|
|
83
168
|
respond(msg.id, null);
|
|
84
169
|
break;
|
|
@@ -94,42 +179,36 @@ function handleMessage(msg) {
|
|
|
94
179
|
|
|
95
180
|
function validateDocument(uri) {
|
|
96
181
|
const text = documents.get(uri);
|
|
97
|
-
if (
|
|
182
|
+
if (text === undefined) return;
|
|
98
183
|
|
|
99
184
|
const diagnostics = [];
|
|
100
185
|
|
|
101
186
|
try {
|
|
102
|
-
const result = compile(text);
|
|
103
|
-
|
|
187
|
+
const result = compile(text, { typeCheck: true });
|
|
104
188
|
const lines = text.split('\n');
|
|
105
|
-
for (
|
|
106
|
-
const line =
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
severity: 2, source: 'naide',
|
|
129
|
-
message: `Type hint: value may not be boolean`
|
|
130
|
-
});
|
|
131
|
-
}
|
|
132
|
-
}
|
|
189
|
+
for (const diagnostic of result.typeErrors.errors) {
|
|
190
|
+
const line = Math.max(0, diagnostic.line - 1);
|
|
191
|
+
diagnostics.push({
|
|
192
|
+
range: {
|
|
193
|
+
start: { line, character: 0 },
|
|
194
|
+
end: { line, character: lines[line]?.length || 0 },
|
|
195
|
+
},
|
|
196
|
+
severity: 1,
|
|
197
|
+
source: 'naide',
|
|
198
|
+
message: diagnostic.message,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
for (const diagnostic of result.typeErrors.warnings) {
|
|
202
|
+
const line = Math.max(0, diagnostic.line - 1);
|
|
203
|
+
diagnostics.push({
|
|
204
|
+
range: {
|
|
205
|
+
start: { line, character: 0 },
|
|
206
|
+
end: { line, character: lines[line]?.length || 0 },
|
|
207
|
+
},
|
|
208
|
+
severity: 2,
|
|
209
|
+
source: 'naide',
|
|
210
|
+
message: diagnostic.message,
|
|
211
|
+
});
|
|
133
212
|
}
|
|
134
213
|
} catch (e) {
|
|
135
214
|
const lineMatch = e.message.match(/line (\d+):(\d+)/);
|
|
@@ -148,8 +227,15 @@ function validateDocument(uri) {
|
|
|
148
227
|
}
|
|
149
228
|
|
|
150
229
|
function indexSymbols(uri, text) {
|
|
151
|
-
const symbols = { definitions: new Map(), references: new Map() };
|
|
230
|
+
const symbols = { definitions: new Map(), references: new Map(), identifiers: [] };
|
|
152
231
|
const lines = text.split('\n');
|
|
232
|
+
let tokens;
|
|
233
|
+
try {
|
|
234
|
+
tokens = new Lexer(text).tokenize();
|
|
235
|
+
} catch {
|
|
236
|
+
symbolIndex.delete(uri);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
153
239
|
|
|
154
240
|
for (let i = 0; i < lines.length; i++) {
|
|
155
241
|
const line = lines[i];
|
|
@@ -190,46 +276,59 @@ function indexSymbols(uri, text) {
|
|
|
190
276
|
symbols.definitions.set(name, { line: i, col, kind: 'prompt' });
|
|
191
277
|
}
|
|
192
278
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const declarationPositions = new Set(
|
|
282
|
+
[...symbols.definitions.entries()].map(([name, location]) =>
|
|
283
|
+
`${location.line}:${location.col}:${name}`,
|
|
284
|
+
),
|
|
285
|
+
);
|
|
286
|
+
for (const [index, token] of tokens.entries()) {
|
|
287
|
+
if (token.type !== 'IDENT') continue;
|
|
288
|
+
const line = token.line - 1;
|
|
289
|
+
const col = token.col - 1;
|
|
290
|
+
symbols.identifiers.push({ name: token.value, line, col });
|
|
291
|
+
const previous = tokens[index - 1];
|
|
292
|
+
const next = tokens[index + 1];
|
|
293
|
+
if (previous?.type === 'DOT' || previous?.type === 'OPTIONAL' || next?.type === 'COLON') continue;
|
|
294
|
+
if (declarationPositions.has(`${line}:${col}:${token.value}`)) continue;
|
|
295
|
+
if (!symbols.references.has(token.value)) symbols.references.set(token.value, []);
|
|
296
|
+
symbols.references.get(token.value).push({ line, col });
|
|
202
297
|
}
|
|
203
298
|
|
|
204
299
|
symbolIndex.set(uri, symbols);
|
|
205
300
|
}
|
|
206
301
|
|
|
207
|
-
function
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
while (end < lineText.length && /\w/.test(lineText[end])) end++;
|
|
214
|
-
return lineText.slice(start, end) || null;
|
|
302
|
+
function getIdentifierAtPosition(symbols, position) {
|
|
303
|
+
return symbols.identifiers?.find(identifier =>
|
|
304
|
+
identifier.line === position.line
|
|
305
|
+
&& position.character >= identifier.col
|
|
306
|
+
&& position.character < identifier.col + identifier.name.length,
|
|
307
|
+
)?.name || null;
|
|
215
308
|
}
|
|
216
309
|
|
|
217
310
|
function getDefinition(params) {
|
|
218
311
|
const uri = params.textDocument.uri;
|
|
219
|
-
const text = documents.get(uri);
|
|
220
|
-
if (!text) return null;
|
|
221
|
-
|
|
222
|
-
const word = getWordAtPosition(text, params.position.line, params.position.character);
|
|
223
|
-
if (!word) return null;
|
|
224
|
-
|
|
225
312
|
const symbols = symbolIndex.get(uri);
|
|
226
313
|
if (!symbols) return null;
|
|
314
|
+
const word = getIdentifierAtPosition(symbols, params.position);
|
|
315
|
+
if (!word) return null;
|
|
227
316
|
|
|
228
|
-
|
|
317
|
+
let definitionUri = uri;
|
|
318
|
+
let def = symbols.definitions.get(word);
|
|
319
|
+
if (!def) {
|
|
320
|
+
for (const [candidateUri, index] of symbolIndex) {
|
|
321
|
+
def = index.definitions.get(word);
|
|
322
|
+
if (def) {
|
|
323
|
+
definitionUri = candidateUri;
|
|
324
|
+
break;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
229
328
|
if (!def) return null;
|
|
230
329
|
|
|
231
330
|
return {
|
|
232
|
-
uri,
|
|
331
|
+
uri: definitionUri,
|
|
233
332
|
range: {
|
|
234
333
|
start: { line: def.line, character: def.col },
|
|
235
334
|
end: { line: def.line, character: def.col + word.length },
|
|
@@ -239,43 +338,40 @@ function getDefinition(params) {
|
|
|
239
338
|
|
|
240
339
|
function getReferences(params) {
|
|
241
340
|
const uri = params.textDocument.uri;
|
|
242
|
-
const text = documents.get(uri);
|
|
243
|
-
if (!text) return [];
|
|
244
|
-
|
|
245
|
-
const word = getWordAtPosition(text, params.position.line, params.position.character);
|
|
246
|
-
if (!word) return [];
|
|
247
|
-
|
|
248
341
|
const symbols = symbolIndex.get(uri);
|
|
249
342
|
if (!symbols) return [];
|
|
343
|
+
const word = getIdentifierAtPosition(symbols, params.position);
|
|
344
|
+
if (!word) return [];
|
|
250
345
|
|
|
251
346
|
const results = [];
|
|
252
347
|
|
|
253
|
-
const
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
348
|
+
for (const [documentUri, index] of symbolIndex) {
|
|
349
|
+
const def = index.definitions.get(word);
|
|
350
|
+
if (def) {
|
|
351
|
+
results.push({
|
|
352
|
+
uri: documentUri,
|
|
353
|
+
range: {
|
|
354
|
+
start: { line: def.line, character: def.col },
|
|
355
|
+
end: { line: def.line, character: def.col + word.length },
|
|
356
|
+
},
|
|
357
|
+
});
|
|
358
|
+
}
|
|
263
359
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
}
|
|
272
|
-
}
|
|
360
|
+
for (const ref of index.references.get(word) || []) {
|
|
361
|
+
results.push({
|
|
362
|
+
uri: documentUri,
|
|
363
|
+
range: {
|
|
364
|
+
start: { line: ref.line, character: ref.col },
|
|
365
|
+
end: { line: ref.line, character: ref.col + word.length },
|
|
366
|
+
},
|
|
367
|
+
});
|
|
368
|
+
}
|
|
273
369
|
}
|
|
274
370
|
|
|
275
371
|
return results;
|
|
276
372
|
}
|
|
277
373
|
|
|
278
|
-
function getCompletions() {
|
|
374
|
+
function getCompletions(params = {}) {
|
|
279
375
|
const keywords = [
|
|
280
376
|
'fn', 'ret', 'if', 'elif', 'else', 'each', 'for', 'while', 'match',
|
|
281
377
|
'try', 'fail', 'ensure', 'server', 'model', 'schema', 'use', 'pub', 'mut',
|
|
@@ -368,12 +464,28 @@ function getCompletions() {
|
|
|
368
464
|
{ label: 'init(params):', detail: 'Constructor method', insertText: 'init(' },
|
|
369
465
|
];
|
|
370
466
|
|
|
371
|
-
|
|
467
|
+
const items = [
|
|
372
468
|
...keywords.map(k => ({ label: k, kind: 14 })),
|
|
373
469
|
...types.map(t => ({ label: t, kind: 25 })),
|
|
374
470
|
...features.map(f => ({ label: f, kind: 14 })),
|
|
375
471
|
...builtins.map(b => ({ label: b.label, kind: 3, detail: b.detail, insertText: b.insertText })),
|
|
376
472
|
];
|
|
473
|
+
const uri = params.textDocument?.uri;
|
|
474
|
+
const text = uri ? documents.get(uri) : null;
|
|
475
|
+
const lineText = text?.split('\n')[params.position?.line] || '';
|
|
476
|
+
const beforeCursor = lineText.slice(0, params.position?.character || 0);
|
|
477
|
+
const prefix = beforeCursor.match(/[A-Za-z_]\w*$/)?.[0] || '';
|
|
478
|
+
if (uri) {
|
|
479
|
+
for (const index of symbolIndex.values()) {
|
|
480
|
+
for (const identifier of index.identifiers || []) {
|
|
481
|
+
items.push({ label: identifier.name, kind: 6, detail: 'Project symbol' });
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
const uniqueItems = [...new Map(items.map(item => [item.label, item])).values()];
|
|
486
|
+
return prefix
|
|
487
|
+
? uniqueItems.filter(item => item.label.startsWith(prefix))
|
|
488
|
+
: uniqueItems;
|
|
377
489
|
}
|
|
378
490
|
|
|
379
491
|
const HOVER_DOCS = {
|
|
@@ -438,10 +550,8 @@ function getHover(params) {
|
|
|
438
550
|
const line = text.split('\n')[params.position.line];
|
|
439
551
|
if (!line) return null;
|
|
440
552
|
const col = params.position.character;
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
while (end < line.length && /\w/.test(line[end])) end++;
|
|
444
|
-
const word = line.slice(start, end);
|
|
553
|
+
const symbols = symbolIndex.get(params.textDocument.uri);
|
|
554
|
+
const word = symbols ? getIdentifierAtPosition(symbols, params.position) : null;
|
|
445
555
|
if (HOVER_DOCS[word]) return { contents: { kind: 'markdown', value: HOVER_DOCS[word] } };
|
|
446
556
|
return null;
|
|
447
557
|
}
|