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/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 += chunk;
11
+ buffer = Buffer.concat([buffer, chunk]);
10
12
  while (true) {
11
- const headerEnd = buffer.indexOf('\r\n\r\n');
12
- if (headerEnd === -1) break;
13
- const header = buffer.slice(0, headerEnd);
14
- const lengthMatch = header.match(/Content-Length: (\d+)/);
15
- if (!lengthMatch) { buffer = buffer.slice(headerEnd + 4); continue; }
16
- const contentLength = parseInt(lengthMatch[1]);
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.slice(start, start + contentLength);
20
- buffer = buffer.slice(start + contentLength);
21
- try { handleMessage(JSON.parse(content)); } catch {}
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 (!text) return;
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 (let i = 0; i < lines.length; i++) {
106
- const line = lines[i].trim();
107
- if (!line || line.startsWith('#') || line.startsWith('--')) continue;
108
-
109
- const typeMatch = line.match(/^(str|int|num|bool|auto)\s+\w+\s*=\s*(.+)/);
110
- if (typeMatch) {
111
- const declType = typeMatch[1];
112
- const val = typeMatch[2].trim();
113
- if (declType === 'int' && /^["']/.test(val)) {
114
- diagnostics.push({
115
- range: { start: { line: i, character: 0 }, end: { line: i, character: lines[i].length } },
116
- severity: 2, source: 'naide',
117
- message: `Type hint: assigning string to int variable`
118
- });
119
- } else if (declType === 'str' && /^\d+$/.test(val)) {
120
- diagnostics.push({
121
- range: { start: { line: i, character: 0 }, end: { line: i, character: lines[i].length } },
122
- severity: 2, source: 'naide',
123
- message: `Type hint: assigning number to str variable`
124
- });
125
- } else if (declType === 'bool' && !['true', 'false'].includes(val) && !/\b(not|and|or|==|!=|>|<)\b/.test(val)) {
126
- diagnostics.push({
127
- range: { start: { line: i, character: 0 }, end: { line: i, character: lines[i].length } },
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
- for (const def of symbols.definitions.keys()) {
194
- const re = new RegExp(`\\b${def}\\b`, 'g');
195
- let m;
196
- while ((m = re.exec(line)) !== null) {
197
- if (i === symbols.definitions.get(def)?.line && m.index === symbols.definitions.get(def)?.col) continue;
198
- if (!symbols.references.has(def)) symbols.references.set(def, []);
199
- symbols.references.get(def).push({ line: i, col: m.index });
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 getWordAtPosition(text, line, col) {
208
- const lines = text.split('\n');
209
- if (line >= lines.length) return null;
210
- const lineText = lines[line];
211
- let start = col, end = col;
212
- while (start > 0 && /\w/.test(lineText[start - 1])) start--;
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
- const def = symbols.definitions.get(word);
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 def = symbols.definitions.get(word);
254
- if (def) {
255
- results.push({
256
- uri,
257
- range: {
258
- start: { line: def.line, character: def.col },
259
- end: { line: def.line, character: def.col + word.length },
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
- const refs = symbols.references.get(word) || [];
265
- for (const ref of refs) {
266
- results.push({
267
- uri,
268
- range: {
269
- start: { line: ref.line, character: ref.col },
270
- end: { line: ref.line, character: ref.col + word.length },
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
- return [
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
- let start = col, end = col;
442
- while (start > 0 && /\w/.test(line[start - 1])) start--;
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
  }