naider 1.24.5 → 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.
@@ -32,19 +32,14 @@ function registerWindows() {
32
32
  const regAdd = (key, value) => run(`reg add "${key}" /ve /d "${value}" /f`);
33
33
 
34
34
  regAdd('HKCU\\Software\\Classes\\.naide', 'NAIDEFile');
35
- regAdd('HKCU\\Software\\Classes\\.nx', 'NAIDEXFile');
36
35
  regAdd('HKCU\\Software\\Classes\\NAIDEFile', 'NAIDE Source File');
37
- regAdd('HKCU\\Software\\Classes\\NAIDEXFile', 'NAIDE-X Source File');
38
36
  regAdd('HKCU\\Software\\Classes\\NAIDEFile\\shell\\open\\command', cmd);
39
- regAdd('HKCU\\Software\\Classes\\NAIDEXFile\\shell\\open\\command', cmd);
40
37
 
41
38
  const assetsDir = resolve(__dirname, '..', 'assets');
42
39
  const naideIco = resolve(assetsDir, 'naide.ico');
43
- const nxIco = resolve(assetsDir, 'nx.ico');
44
40
  regAdd('HKCU\\Software\\Classes\\NAIDEFile\\DefaultIcon', naideIco);
45
- regAdd('HKCU\\Software\\Classes\\NAIDEXFile\\DefaultIcon', nxIco);
46
41
 
47
- console.log(' .naide and .nx file associations registered (Windows)');
42
+ console.log(' .naide file association registered (Windows)');
48
43
  }
49
44
 
50
45
  function registerMacOS() {
@@ -68,7 +63,7 @@ function registerMacOS() {
68
63
  <key>CFBundleDocumentTypes</key>
69
64
  <array>
70
65
  <dict>
71
- <key>CFBundleTypeExtensions</key><array><string>naide</string><string>nx</string></array>
66
+ <key>CFBundleTypeExtensions</key><array><string>naide</string></array>
72
67
  <key>CFBundleTypeName</key><string>NAIDE Source</string>
73
68
  <key>CFBundleTypeRole</key><string>Editor</string>
74
69
  </dict>
@@ -81,7 +76,7 @@ function registerMacOS() {
81
76
 
82
77
  run(`/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister -f "${handlerApp}"`);
83
78
 
84
- console.log(' .naide and .nx file associations registered (macOS)');
79
+ console.log(' .naide file association registered (macOS)');
85
80
  }
86
81
 
87
82
  function registerLinux() {
@@ -96,26 +91,21 @@ function registerLinux() {
96
91
  <comment>NAIDE Source File</comment>
97
92
  <glob pattern="*.naide"/>
98
93
  </mime-type>
99
- <mime-type type="text/x-naidex">
100
- <comment>NAIDE-X Source File</comment>
101
- <glob pattern="*.nx"/>
102
- </mime-type>
103
94
  </mime-info>`);
104
95
 
105
96
  writeFileSync(join(appDir, 'naide.desktop'), `[Desktop Entry]
106
97
  Type=Application
107
98
  Name=NAIDE
108
99
  Exec=node "${naideBin}" %f
109
- MimeType=text/x-naide;text/x-naidex;
100
+ MimeType=text/x-naide;
110
101
  Terminal=true
111
102
  Categories=Development;
112
103
  `);
113
104
 
114
105
  run('update-mime-database ~/.local/share/mime');
115
106
  run('xdg-mime default naide.desktop text/x-naide');
116
- run('xdg-mime default naide.desktop text/x-naidex');
117
107
 
118
- console.log(' .naide and .nx file associations registered (Linux)');
108
+ console.log(' .naide file association registered (Linux)');
119
109
  }
120
110
 
121
111
  console.log('NAIDE: Registering file extensions...');
@@ -1,5 +1,5 @@
1
1
  # NAIDE - Model (Class) の例
2
- # AIがクラスを最速で書ける構文
2
+ # NAIDEでクラスの継承を表す例
3
3
 
4
4
  model Animal:
5
5
  str name
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,43 +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
- const mode = uri.endsWith('.nx') ? 'x' : 'naide';
100
184
  const diagnostics = [];
101
185
 
102
186
  try {
103
- const result = compile(text, { mode });
104
-
187
+ const result = compile(text, { typeCheck: true });
105
188
  const lines = text.split('\n');
106
- for (let i = 0; i < lines.length; i++) {
107
- const line = lines[i].trim();
108
- if (!line || line.startsWith('#') || line.startsWith('--')) continue;
109
-
110
- const typeMatch = line.match(/^(str|int|num|bool|auto)\s+\w+\s*=\s*(.+)/);
111
- if (typeMatch) {
112
- const declType = typeMatch[1];
113
- const val = typeMatch[2].trim();
114
- if (declType === 'int' && /^["']/.test(val)) {
115
- diagnostics.push({
116
- range: { start: { line: i, character: 0 }, end: { line: i, character: lines[i].length } },
117
- severity: 2, source: 'naide',
118
- message: `Type hint: assigning string to int variable`
119
- });
120
- } else if (declType === 'str' && /^\d+$/.test(val)) {
121
- diagnostics.push({
122
- range: { start: { line: i, character: 0 }, end: { line: i, character: lines[i].length } },
123
- severity: 2, source: 'naide',
124
- message: `Type hint: assigning number to str variable`
125
- });
126
- } else if (declType === 'bool' && !['true', 'false'].includes(val) && !/\b(not|and|or|==|!=|>|<)\b/.test(val)) {
127
- diagnostics.push({
128
- range: { start: { line: i, character: 0 }, end: { line: i, character: lines[i].length } },
129
- severity: 2, source: 'naide',
130
- message: `Type hint: value may not be boolean`
131
- });
132
- }
133
- }
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
+ });
134
212
  }
135
213
  } catch (e) {
136
214
  const lineMatch = e.message.match(/line (\d+):(\d+)/);
@@ -149,8 +227,15 @@ function validateDocument(uri) {
149
227
  }
150
228
 
151
229
  function indexSymbols(uri, text) {
152
- const symbols = { definitions: new Map(), references: new Map() };
230
+ const symbols = { definitions: new Map(), references: new Map(), identifiers: [] };
153
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
+ }
154
239
 
155
240
  for (let i = 0; i < lines.length; i++) {
156
241
  const line = lines[i];
@@ -191,46 +276,59 @@ function indexSymbols(uri, text) {
191
276
  symbols.definitions.set(name, { line: i, col, kind: 'prompt' });
192
277
  }
193
278
 
194
- for (const def of symbols.definitions.keys()) {
195
- const re = new RegExp(`\\b${def}\\b`, 'g');
196
- let m;
197
- while ((m = re.exec(line)) !== null) {
198
- if (i === symbols.definitions.get(def)?.line && m.index === symbols.definitions.get(def)?.col) continue;
199
- if (!symbols.references.has(def)) symbols.references.set(def, []);
200
- symbols.references.get(def).push({ line: i, col: m.index });
201
- }
202
- }
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 });
203
297
  }
204
298
 
205
299
  symbolIndex.set(uri, symbols);
206
300
  }
207
301
 
208
- function getWordAtPosition(text, line, col) {
209
- const lines = text.split('\n');
210
- if (line >= lines.length) return null;
211
- const lineText = lines[line];
212
- let start = col, end = col;
213
- while (start > 0 && /\w/.test(lineText[start - 1])) start--;
214
- while (end < lineText.length && /\w/.test(lineText[end])) end++;
215
- 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;
216
308
  }
217
309
 
218
310
  function getDefinition(params) {
219
311
  const uri = params.textDocument.uri;
220
- const text = documents.get(uri);
221
- if (!text) return null;
222
-
223
- const word = getWordAtPosition(text, params.position.line, params.position.character);
224
- if (!word) return null;
225
-
226
312
  const symbols = symbolIndex.get(uri);
227
313
  if (!symbols) return null;
314
+ const word = getIdentifierAtPosition(symbols, params.position);
315
+ if (!word) return null;
228
316
 
229
- 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
+ }
230
328
  if (!def) return null;
231
329
 
232
330
  return {
233
- uri,
331
+ uri: definitionUri,
234
332
  range: {
235
333
  start: { line: def.line, character: def.col },
236
334
  end: { line: def.line, character: def.col + word.length },
@@ -240,43 +338,40 @@ function getDefinition(params) {
240
338
 
241
339
  function getReferences(params) {
242
340
  const uri = params.textDocument.uri;
243
- const text = documents.get(uri);
244
- if (!text) return [];
245
-
246
- const word = getWordAtPosition(text, params.position.line, params.position.character);
247
- if (!word) return [];
248
-
249
341
  const symbols = symbolIndex.get(uri);
250
342
  if (!symbols) return [];
343
+ const word = getIdentifierAtPosition(symbols, params.position);
344
+ if (!word) return [];
251
345
 
252
346
  const results = [];
253
347
 
254
- const def = symbols.definitions.get(word);
255
- if (def) {
256
- results.push({
257
- uri,
258
- range: {
259
- start: { line: def.line, character: def.col },
260
- end: { line: def.line, character: def.col + word.length },
261
- },
262
- });
263
- }
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
+ }
264
359
 
265
- const refs = symbols.references.get(word) || [];
266
- for (const ref of refs) {
267
- results.push({
268
- uri,
269
- range: {
270
- start: { line: ref.line, character: ref.col },
271
- end: { line: ref.line, character: ref.col + word.length },
272
- },
273
- });
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
+ }
274
369
  }
275
370
 
276
371
  return results;
277
372
  }
278
373
 
279
- function getCompletions() {
374
+ function getCompletions(params = {}) {
280
375
  const keywords = [
281
376
  'fn', 'ret', 'if', 'elif', 'else', 'each', 'for', 'while', 'match',
282
377
  'try', 'fail', 'ensure', 'server', 'model', 'schema', 'use', 'pub', 'mut',
@@ -369,12 +464,28 @@ function getCompletions() {
369
464
  { label: 'init(params):', detail: 'Constructor method', insertText: 'init(' },
370
465
  ];
371
466
 
372
- return [
467
+ const items = [
373
468
  ...keywords.map(k => ({ label: k, kind: 14 })),
374
469
  ...types.map(t => ({ label: t, kind: 25 })),
375
470
  ...features.map(f => ({ label: f, kind: 14 })),
376
471
  ...builtins.map(b => ({ label: b.label, kind: 3, detail: b.detail, insertText: b.insertText })),
377
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;
378
489
  }
379
490
 
380
491
  const HOVER_DOCS = {
@@ -439,10 +550,8 @@ function getHover(params) {
439
550
  const line = text.split('\n')[params.position.line];
440
551
  if (!line) return null;
441
552
  const col = params.position.character;
442
- let start = col, end = col;
443
- while (start > 0 && /\w/.test(line[start - 1])) start--;
444
- while (end < line.length && /\w/.test(line[end])) end++;
445
- const word = line.slice(start, end);
553
+ const symbols = symbolIndex.get(params.textDocument.uri);
554
+ const word = symbols ? getIdentifierAtPosition(symbols, params.position) : null;
446
555
  if (HOVER_DOCS[word]) return { contents: { kind: 'markdown', value: HOVER_DOCS[word] } };
447
556
  return null;
448
557
  }