nene-lang 0.1.0 → 0.1.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/README.md +29 -13
- package/build/actions.d.ts +22 -0
- package/build/actions.js +124 -0
- package/build/checker.js +2 -2
- package/build/cli.js +46 -6
- package/build/compiler.d.ts +2 -0
- package/build/compiler.js +7 -1
- package/build/format.d.ts +1 -0
- package/build/format.js +120 -0
- package/build/lsp.js +263 -3
- package/build/outline.d.ts +49 -0
- package/build/outline.js +298 -0
- package/build/packages.js +1 -1
- package/build/prerender.d.ts +8 -0
- package/build/prerender.js +62 -0
- package/build/references.d.ts +13 -0
- package/build/references.js +48 -0
- package/docs/design.md +8 -3
- package/docs/guide.md +14 -3
- package/docs/spec.md +25 -3
- package/editors/nvim/lua/nene.lua +38 -14
- package/package.json +3 -3
- package/runtime/runtime.js +53 -9
- package/template/CLAUDE.md +4 -2
- package/template/README.md +3 -2
- package/template/package.json +3 -2
- package/template/src/Main.nn +2 -2
package/build/lsp.js
CHANGED
|
@@ -4,14 +4,25 @@
|
|
|
4
4
|
// できること:
|
|
5
5
|
// - 保存前の内容も含めて型検査し、エラーを表示する(diagnostics)
|
|
6
6
|
// - 名前やタグにカーソルを置くと型を表示する(hover)
|
|
7
|
-
// - 定義へ移動する(definition)
|
|
7
|
+
// - 定義へ移動する(definition)、参照一覧(references)、名前の一括変更(rename)
|
|
8
8
|
// - 補完(タグと必須の属性・閉じタグ、属性、import するモジュール、フィールド、名前)
|
|
9
|
+
// - 関数の引数のヒント(signatureHelp)
|
|
9
10
|
// - 色分け(部品と HTML 要素、コンストラクタ、フィールド、引数などを区別する semantic tokens)
|
|
11
|
+
// - アウトライン(documentSymbol)、プロジェクト全体のシンボル検索(workspace/symbol)
|
|
12
|
+
// - 折りたたみ(foldingRange)、構造に沿った選択(selectionRange)
|
|
13
|
+
// - 整形(formatting。nene fmt と同じ)
|
|
14
|
+
// - コードアクション(部品の import を足す、JSX の {} の式を where に取り出す)
|
|
15
|
+
import { readFileSync } from "node:fs";
|
|
10
16
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
11
|
-
import {
|
|
17
|
+
import { codeActions } from "./actions.js";
|
|
18
|
+
import { compileProgram, listModules, projectFiles, projectRoot } from "./compiler.js";
|
|
19
|
+
import { format } from "./format.js";
|
|
20
|
+
import { foldingRanges, outline, parse, selectionRange } from "./outline.js";
|
|
21
|
+
import { definitionOf, referencesOf, renameProblem, replacementFor } from "./references.js";
|
|
12
22
|
import { HTML_ATTRS, HTML_ELEMENTS } from "./checker.js";
|
|
13
23
|
import { CompileError } from "./syntax.js";
|
|
14
24
|
import { flattenRecord, prune } from "./types.js";
|
|
25
|
+
const VERSION = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
|
|
15
26
|
const docs = new Map(); // uri → 編集中の内容
|
|
16
27
|
const results = new Map(); // uri → 最後に成功した検査結果と、そのときの内容
|
|
17
28
|
const timers = new Map();
|
|
@@ -101,6 +112,188 @@ function schedule(uri) {
|
|
|
101
112
|
clearTimeout(timers.get(uri));
|
|
102
113
|
timers.set(uri, setTimeout(() => validate(uri), 150));
|
|
103
114
|
}
|
|
115
|
+
// ---- プロジェクト全体 ----
|
|
116
|
+
function textOf(uri) {
|
|
117
|
+
const open = docs.get(uri);
|
|
118
|
+
if (open !== undefined)
|
|
119
|
+
return open;
|
|
120
|
+
try {
|
|
121
|
+
return readFileSync(fileURLToPath(uri), "utf8");
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
return "";
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// このファイルのプロジェクトのフォルダ(src/ など。モジュール名から求める)
|
|
128
|
+
function rootOf(uri) {
|
|
129
|
+
const name = /^module\s+([\w.]+)/m.exec(textOf(uri))?.[1];
|
|
130
|
+
return name ? projectRoot(fileURLToPath(uri), name) : null;
|
|
131
|
+
}
|
|
132
|
+
// プロジェクトのすべてのモジュールを、編集中の内容で型検査したもの。型検査の通らないモジュールは除く
|
|
133
|
+
function projectModules(uri) {
|
|
134
|
+
const root = rootOf(uri);
|
|
135
|
+
if (!root)
|
|
136
|
+
return compiledModule(uri) ? [compiledModule(uri)] : [];
|
|
137
|
+
const overrides = new Map();
|
|
138
|
+
for (const [u, text] of docs)
|
|
139
|
+
overrides.set(fileURLToPath(u), text);
|
|
140
|
+
const seen = new Map();
|
|
141
|
+
for (const file of projectFiles(root)) {
|
|
142
|
+
if (seen.has(file))
|
|
143
|
+
continue;
|
|
144
|
+
try {
|
|
145
|
+
for (const m of compileProgram(file, { overrides, requireMain: false }).modules)
|
|
146
|
+
seen.set(m.file, m);
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
// エラーのあるモジュールは、参照を数えられない
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return [...seen.values()];
|
|
153
|
+
}
|
|
154
|
+
const inside = (file, root) => file.startsWith(root.endsWith("/") ? root : root + "/");
|
|
155
|
+
function locationOf(o) {
|
|
156
|
+
return { uri: pathToFileURL(o.file).href, range: toRange(o.pos, o.length) };
|
|
157
|
+
}
|
|
158
|
+
function references(uri, line, character) {
|
|
159
|
+
const h = hoverAt(uri, line, character);
|
|
160
|
+
const def = h && definitionOf(h);
|
|
161
|
+
if (!def)
|
|
162
|
+
return [];
|
|
163
|
+
return referencesOf(def, projectModules(uri)).map(locationOf);
|
|
164
|
+
}
|
|
165
|
+
// 名前を変えられるかを確かめ、変える名前の範囲を返す。変えられないときは理由
|
|
166
|
+
function renameTarget(uri, line, character) {
|
|
167
|
+
const h = hoverAt(uri, line, character);
|
|
168
|
+
const def = h && definitionOf(h);
|
|
169
|
+
if (!h || !def || h.label?.().startsWith("module "))
|
|
170
|
+
return "ここには変えられる名前がありません";
|
|
171
|
+
const root = rootOf(uri);
|
|
172
|
+
if (!root || !inside(def.file, root))
|
|
173
|
+
return "標準ライブラリやパッケージの名前は変えられません";
|
|
174
|
+
const name = h.name.slice(h.name.lastIndexOf(".") + 1);
|
|
175
|
+
const file = def.file.split("/").pop().replace(/\.nn$/, "");
|
|
176
|
+
if (name.toLowerCase() === file.toLowerCase())
|
|
177
|
+
return `${name} はファイルと同じ名前の主部品なので、ファイル名と一緒に変えてください`;
|
|
178
|
+
const col = h.pos.col + (h.name.length - name.length);
|
|
179
|
+
return { def, occurrence: { file: fileURLToPath(uri), pos: { line: h.pos.line, col }, length: name.length, text: name } };
|
|
180
|
+
}
|
|
181
|
+
function rename(uri, line, character, newName) {
|
|
182
|
+
const target = renameTarget(uri, line, character);
|
|
183
|
+
if (typeof target === "string")
|
|
184
|
+
throw new LspError(target);
|
|
185
|
+
const refs = referencesOf(target.def, projectModules(uri));
|
|
186
|
+
// 定義に書いてある名前で、新しい名前の種類を確かめる
|
|
187
|
+
const original = refs.find((r) => r.file === target.def.file && r.pos.line === target.def.pos.line)?.text ?? target.occurrence.text;
|
|
188
|
+
const problem = renameProblem(original, newName);
|
|
189
|
+
if (problem)
|
|
190
|
+
throw new LspError(problem);
|
|
191
|
+
const changes = {};
|
|
192
|
+
for (const r of refs) {
|
|
193
|
+
const u = pathToFileURL(r.file).href;
|
|
194
|
+
// 型検査のあとで書き換わっていたら、その場所は変えない
|
|
195
|
+
const lineText = textOf(u).split("\n")[r.pos.line - 1] ?? "";
|
|
196
|
+
if (lineText.slice(r.pos.col - 1, r.pos.col - 1 + r.length) !== r.text)
|
|
197
|
+
continue;
|
|
198
|
+
(changes[u] ??= []).push({ range: toRange(r.pos, r.length), newText: replacementFor(r.text, newName) });
|
|
199
|
+
}
|
|
200
|
+
return { changes };
|
|
201
|
+
}
|
|
202
|
+
class LspError extends Error {
|
|
203
|
+
}
|
|
204
|
+
// ---- シンボル ----
|
|
205
|
+
function workspaceSymbols(query) {
|
|
206
|
+
const anyUri = [...docs.keys()][0];
|
|
207
|
+
const root = anyUri ? rootOf(anyUri) : null;
|
|
208
|
+
if (!root)
|
|
209
|
+
return [];
|
|
210
|
+
const q = query.toLowerCase();
|
|
211
|
+
const out = [];
|
|
212
|
+
for (const file of projectFiles(root)) {
|
|
213
|
+
const uri = pathToFileURL(file).href;
|
|
214
|
+
const text = textOf(uri);
|
|
215
|
+
const mod = parse(text, file);
|
|
216
|
+
if (!mod)
|
|
217
|
+
continue;
|
|
218
|
+
const walk = (symbols, container) => {
|
|
219
|
+
for (const sym of symbols) {
|
|
220
|
+
if (sym.name.toLowerCase().includes(q))
|
|
221
|
+
out.push({ name: sym.name, kind: sym.kind, containerName: container ?? mod.name, location: { uri, range: sym.selectionRange } });
|
|
222
|
+
if (sym.children)
|
|
223
|
+
walk(sym.children, sym.name);
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
walk(outline(text, mod));
|
|
227
|
+
}
|
|
228
|
+
return out.slice(0, 500);
|
|
229
|
+
}
|
|
230
|
+
// ---- 引数のヒント ----
|
|
231
|
+
// カーソルの前にある関数適用(f a b ...)の、関数の型と、いま何番目の引数を書いているか
|
|
232
|
+
function signatureHelp(uri, line, character) {
|
|
233
|
+
const mod = compiledModule(uri);
|
|
234
|
+
if (!mod)
|
|
235
|
+
return null;
|
|
236
|
+
const before = (textOf(uri).split("\n")[line] ?? "").slice(0, character);
|
|
237
|
+
// 開いた括弧や = のあとから、カーソルまでを見る
|
|
238
|
+
let depth = 0;
|
|
239
|
+
let start = before.length;
|
|
240
|
+
for (let i = before.length - 1; i >= 0; i--) {
|
|
241
|
+
const c = before[i];
|
|
242
|
+
if (c === ")" || c === "]" || c === "}")
|
|
243
|
+
depth++;
|
|
244
|
+
else if (c === "(" || c === "[" || c === "{") {
|
|
245
|
+
if (depth === 0)
|
|
246
|
+
break;
|
|
247
|
+
depth--;
|
|
248
|
+
}
|
|
249
|
+
else if (depth === 0 && (c === "=" || c === "," || c === "|" || c === "$"))
|
|
250
|
+
break;
|
|
251
|
+
start = i;
|
|
252
|
+
}
|
|
253
|
+
const expr = before.slice(start);
|
|
254
|
+
const m = /^\s*([A-Z][\w]*(?:\.[A-Z][\w]*)*\.)?([a-z_][\w']*)\s/.exec(expr);
|
|
255
|
+
if (!m)
|
|
256
|
+
return null;
|
|
257
|
+
const name = (m[1] ?? "") + m[2];
|
|
258
|
+
const detail = m[1]
|
|
259
|
+
? mod.completion.quals.get(m[1].slice(0, -1))?.find((q) => q.name === m[2])?.detail
|
|
260
|
+
: mod.completion.values.find((v) => v.name === name)?.detail;
|
|
261
|
+
if (!detail || !detail.includes("->"))
|
|
262
|
+
return null;
|
|
263
|
+
const params = splitArrows(detail);
|
|
264
|
+
params.pop(); // 結果の型
|
|
265
|
+
// 書き終えた引数の数(括弧の中をまとめた単語の数)
|
|
266
|
+
const args = expr.slice(m[0].length).replace(/\([^()]*\)|\[[^\[\]]*\]|\{[^{}]*\}|"[^"]*"/g, "x").trim();
|
|
267
|
+
const written = args === "" ? 0 : args.split(/\s+/).length - (before.endsWith(" ") ? 0 : 1);
|
|
268
|
+
const label = `${name} :: ${detail}`;
|
|
269
|
+
let offset = label.indexOf("::") + 3;
|
|
270
|
+
const parameters = params.map((p) => {
|
|
271
|
+
const at = label.indexOf(p, offset);
|
|
272
|
+
offset = at + p.length;
|
|
273
|
+
return { label: [at, at + p.length] };
|
|
274
|
+
});
|
|
275
|
+
return { signatures: [{ label, parameters }], activeSignature: 0, activeParameter: Math.min(written, Math.max(0, params.length - 1)) };
|
|
276
|
+
}
|
|
277
|
+
// a -> (b -> c) -> d を [a, (b -> c), d] に分ける
|
|
278
|
+
function splitArrows(type) {
|
|
279
|
+
const out = [];
|
|
280
|
+
let depth = 0;
|
|
281
|
+
let start = 0;
|
|
282
|
+
for (let i = 0; i < type.length; i++) {
|
|
283
|
+
const c = type[i];
|
|
284
|
+
if (c === "(" || c === "[" || c === "{")
|
|
285
|
+
depth++;
|
|
286
|
+
else if (c === ")" || c === "]" || c === "}")
|
|
287
|
+
depth--;
|
|
288
|
+
else if (depth === 0 && type.startsWith(" -> ", i)) {
|
|
289
|
+
out.push(type.slice(start, i).trim());
|
|
290
|
+
start = i + 4;
|
|
291
|
+
i += 3;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
out.push(type.slice(start).trim());
|
|
295
|
+
return out;
|
|
296
|
+
}
|
|
104
297
|
// ---- 色分け ----
|
|
105
298
|
const TOKEN_TYPES = ["namespace", "type", "class", "enumMember", "function", "variable", "parameter", "property", "macro"];
|
|
106
299
|
function semanticTokens(uri) {
|
|
@@ -247,8 +440,17 @@ function handle(msg) {
|
|
|
247
440
|
definitionProvider: true,
|
|
248
441
|
completionProvider: { triggerCharacters: ["<", ".", " "] },
|
|
249
442
|
semanticTokensProvider: { legend: { tokenTypes: TOKEN_TYPES, tokenModifiers: [] }, full: true },
|
|
443
|
+
referencesProvider: true,
|
|
444
|
+
renameProvider: { prepareProvider: true },
|
|
445
|
+
documentSymbolProvider: true,
|
|
446
|
+
workspaceSymbolProvider: true,
|
|
447
|
+
foldingRangeProvider: true,
|
|
448
|
+
selectionRangeProvider: true,
|
|
449
|
+
documentFormattingProvider: true,
|
|
450
|
+
codeActionProvider: { codeActionKinds: ["quickfix", "refactor.extract"] },
|
|
451
|
+
signatureHelpProvider: { triggerCharacters: [" "], retriggerCharacters: [" "] },
|
|
250
452
|
},
|
|
251
|
-
serverInfo: { name: "nene", version:
|
|
453
|
+
serverInfo: { name: "nene", version: VERSION },
|
|
252
454
|
});
|
|
253
455
|
return;
|
|
254
456
|
case "textDocument/completion":
|
|
@@ -286,6 +488,64 @@ function handle(msg) {
|
|
|
286
488
|
});
|
|
287
489
|
return;
|
|
288
490
|
}
|
|
491
|
+
case "textDocument/references":
|
|
492
|
+
respond(id, references(params.textDocument.uri, params.position.line, params.position.character));
|
|
493
|
+
return;
|
|
494
|
+
case "textDocument/prepareRename": {
|
|
495
|
+
const target = renameTarget(params.textDocument.uri, params.position.line, params.position.character);
|
|
496
|
+
if (typeof target === "string")
|
|
497
|
+
send({ id, error: { code: -32803, message: target } });
|
|
498
|
+
else
|
|
499
|
+
respond(id, { range: toRange(target.occurrence.pos, target.occurrence.length), placeholder: target.occurrence.text });
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
case "textDocument/rename":
|
|
503
|
+
try {
|
|
504
|
+
respond(id, rename(params.textDocument.uri, params.position.line, params.position.character, params.newName));
|
|
505
|
+
}
|
|
506
|
+
catch (e) {
|
|
507
|
+
if (!(e instanceof LspError))
|
|
508
|
+
throw e;
|
|
509
|
+
send({ id, error: { code: -32803, message: e.message } });
|
|
510
|
+
}
|
|
511
|
+
return;
|
|
512
|
+
case "textDocument/documentSymbol": {
|
|
513
|
+
const uri = params.textDocument.uri;
|
|
514
|
+
const mod = parse(textOf(uri), fileURLToPath(uri));
|
|
515
|
+
respond(id, mod ? outline(textOf(uri), mod) : []);
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
case "workspace/symbol":
|
|
519
|
+
respond(id, workspaceSymbols(params.query ?? ""));
|
|
520
|
+
return;
|
|
521
|
+
case "textDocument/foldingRange": {
|
|
522
|
+
const uri = params.textDocument.uri;
|
|
523
|
+
respond(id, foldingRanges(textOf(uri), parse(textOf(uri), fileURLToPath(uri))));
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
case "textDocument/selectionRange": {
|
|
527
|
+
const uri = params.textDocument.uri;
|
|
528
|
+
const mod = parse(textOf(uri), fileURLToPath(uri));
|
|
529
|
+
respond(id, params.positions.map((p) => selectionRange(textOf(uri), mod, p.line, p.character) ?? { range: { start: p, end: p } }));
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
case "textDocument/formatting": {
|
|
533
|
+
const text = textOf(params.textDocument.uri);
|
|
534
|
+
const formatted = format(text);
|
|
535
|
+
const lines = text.split("\n");
|
|
536
|
+
const whole = { start: { line: 0, character: 0 }, end: { line: lines.length, character: 0 } };
|
|
537
|
+
respond(id, formatted === text ? [] : [{ range: whole, newText: formatted }]);
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
case "textDocument/codeAction": {
|
|
541
|
+
const uri = params.textDocument.uri;
|
|
542
|
+
const root = rootOf(uri);
|
|
543
|
+
respond(id, codeActions({ uri, text: textOf(uri), diagnostics: params.context.diagnostics, modules: root ? listModules(root) : [] }));
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
case "textDocument/signatureHelp":
|
|
547
|
+
respond(id, signatureHelp(params.textDocument.uri, params.position.line, params.position.character));
|
|
548
|
+
return;
|
|
289
549
|
case "textDocument/definition": {
|
|
290
550
|
const h = hoverAt(params.textDocument.uri, params.position.line, params.position.character);
|
|
291
551
|
const def = typeof h?.def === "function" ? h.def() : h?.def;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { Module } from "./syntax.ts";
|
|
2
|
+
export interface Range {
|
|
3
|
+
start: {
|
|
4
|
+
line: number;
|
|
5
|
+
character: number;
|
|
6
|
+
};
|
|
7
|
+
end: {
|
|
8
|
+
line: number;
|
|
9
|
+
character: number;
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
export interface OutlineSymbol {
|
|
13
|
+
name: string;
|
|
14
|
+
detail?: string;
|
|
15
|
+
kind: number;
|
|
16
|
+
range: Range;
|
|
17
|
+
selectionRange: Range;
|
|
18
|
+
children?: OutlineSymbol[];
|
|
19
|
+
}
|
|
20
|
+
export declare const SYMBOL: {
|
|
21
|
+
module: number;
|
|
22
|
+
class: number;
|
|
23
|
+
method: number;
|
|
24
|
+
field: number;
|
|
25
|
+
enum: number;
|
|
26
|
+
function: number;
|
|
27
|
+
variable: number;
|
|
28
|
+
enumMember: number;
|
|
29
|
+
struct: number;
|
|
30
|
+
};
|
|
31
|
+
export interface FoldingRange {
|
|
32
|
+
startLine: number;
|
|
33
|
+
endLine: number;
|
|
34
|
+
kind?: "comment" | "imports" | "region";
|
|
35
|
+
}
|
|
36
|
+
export declare function parse(text: string, file: string): Module | null;
|
|
37
|
+
export declare function outline(text: string, mod: Module): OutlineSymbol[];
|
|
38
|
+
interface Span {
|
|
39
|
+
start: number;
|
|
40
|
+
end: number;
|
|
41
|
+
}
|
|
42
|
+
export declare function jsxElements(text: string): Span[];
|
|
43
|
+
export declare function foldingRanges(text: string, mod: Module | null): FoldingRange[];
|
|
44
|
+
export interface SelectionRange {
|
|
45
|
+
range: Range;
|
|
46
|
+
parent?: SelectionRange;
|
|
47
|
+
}
|
|
48
|
+
export declare function selectionRange(text: string, mod: Module | null, line: number, character: number): SelectionRange | null;
|
|
49
|
+
export {};
|
package/build/outline.js
ADDED
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
// ファイルの構造を、エディタの機能のために取り出す
|
|
2
|
+
//
|
|
3
|
+
// - アウトライン(関数・型・コンストラクタ・フィールド・where の中の定義)
|
|
4
|
+
// - 折りたたみ(宣言・where・JSX の要素・import のまとまり・コメント・style ブロック)
|
|
5
|
+
// - 構造に沿った選択(名前 → JSX の要素 → 等式 → 宣言 と広げる)
|
|
6
|
+
//
|
|
7
|
+
// 型検査が通らない書きかけのファイルでも使えるよう、構文だけを見る。
|
|
8
|
+
import { parseModule } from "./parser.js";
|
|
9
|
+
export const SYMBOL = { module: 2, class: 5, method: 6, field: 8, enum: 10, function: 12, variable: 13, enumMember: 22, struct: 23 };
|
|
10
|
+
// 構文が読めなければ null
|
|
11
|
+
export function parse(text, file) {
|
|
12
|
+
try {
|
|
13
|
+
return parseModule(text, file);
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
// ---- アウトライン ----
|
|
20
|
+
export function outline(text, mod) {
|
|
21
|
+
const lines = text.split("\n");
|
|
22
|
+
const ends = declEnds(lines, mod);
|
|
23
|
+
const range = (pos) => ({
|
|
24
|
+
start: { line: pos.line - 1, character: pos.col - 1 },
|
|
25
|
+
end: { line: ends(pos.line - 1), character: lines[ends(pos.line - 1)]?.length ?? 0 },
|
|
26
|
+
});
|
|
27
|
+
const name = (pos, n) => ({
|
|
28
|
+
start: { line: pos.line - 1, character: pos.col - 1 },
|
|
29
|
+
end: { line: pos.line - 1, character: pos.col - 1 + n.length },
|
|
30
|
+
});
|
|
31
|
+
const symbols = [];
|
|
32
|
+
for (const d of mod.datas) {
|
|
33
|
+
symbols.push({
|
|
34
|
+
name: d.name,
|
|
35
|
+
detail: d.params.join(" "),
|
|
36
|
+
kind: SYMBOL.enum,
|
|
37
|
+
range: range(d.pos),
|
|
38
|
+
selectionRange: name(d.namePos, d.name),
|
|
39
|
+
children: d.ctors.map((c) => ({ name: c.name, kind: SYMBOL.enumMember, range: name(c.pos, c.name), selectionRange: name(c.pos, c.name) })),
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
for (const a of mod.aliases) {
|
|
43
|
+
const fields = a.body.k === "trecord" ? a.body.fields : [];
|
|
44
|
+
symbols.push({
|
|
45
|
+
name: a.name,
|
|
46
|
+
kind: SYMBOL.struct,
|
|
47
|
+
range: range(a.pos),
|
|
48
|
+
selectionRange: name(a.namePos, a.name),
|
|
49
|
+
children: fields.map((f) => ({ name: f.name, kind: SYMBOL.field, range: name(f.pos, f.name), selectionRange: name(f.pos, f.name) })),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
for (const f of mod.foreigns) {
|
|
53
|
+
symbols.push({ name: f.name, detail: "foreign", kind: SYMBOL.function, range: range(f.pos), selectionRange: name(f.sig.pos, f.name) });
|
|
54
|
+
}
|
|
55
|
+
for (const b of mod.body.bindings)
|
|
56
|
+
symbols.push(bindingSymbol(b, lines, range(b.sig?.pos ?? b.pos)));
|
|
57
|
+
if (mod.style) {
|
|
58
|
+
const line = mod.style.pos.line - 2; // style と書いた行
|
|
59
|
+
const r = { start: { line, character: 0 }, end: { line: lines.length - 1, character: 0 } };
|
|
60
|
+
symbols.push({ name: "style", kind: SYMBOL.module, range: r, selectionRange: { start: { line, character: 0 }, end: { line, character: 5 } } });
|
|
61
|
+
}
|
|
62
|
+
return symbols.sort((a, b) => a.range.start.line - b.range.start.line);
|
|
63
|
+
}
|
|
64
|
+
function bindingSymbol(b, lines, range) {
|
|
65
|
+
const sigText = b.sig ? (lines[b.sig.pos.line - 1] ?? "").split("::").slice(1).join("::").trim() : undefined;
|
|
66
|
+
const isFunction = b.equations[0].params.length > 0 || (sigText?.includes("->") ?? false);
|
|
67
|
+
const pos = b.equations[0].pos;
|
|
68
|
+
const children = b.equations.flatMap((eq) => eq.where.bindings).map((w) => {
|
|
69
|
+
const first = w.equations[0].pos;
|
|
70
|
+
const last = w.equations[w.equations.length - 1].pos;
|
|
71
|
+
const start = (w.sig?.pos ?? first).line - 1;
|
|
72
|
+
const r = { start: { line: start, character: 0 }, end: { line: blockEnd(lines, last.line - 1), character: 0 } };
|
|
73
|
+
return bindingSymbol(w, lines, r);
|
|
74
|
+
});
|
|
75
|
+
return {
|
|
76
|
+
name: b.name,
|
|
77
|
+
detail: sigText,
|
|
78
|
+
kind: isFunction ? SYMBOL.function : SYMBOL.variable,
|
|
79
|
+
range,
|
|
80
|
+
selectionRange: { start: { line: pos.line - 1, character: pos.col - 1 }, end: { line: pos.line - 1, character: pos.col - 1 + b.name.length } },
|
|
81
|
+
children: children.length > 0 ? children : undefined,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
// トップレベルの宣言が終わる行(次の宣言の手前。末尾の空行とコメントは含めない)
|
|
85
|
+
function declEnds(lines, mod) {
|
|
86
|
+
const starts = [
|
|
87
|
+
...mod.imports.map((d) => d.pos.line - 1),
|
|
88
|
+
...mod.datas.map((d) => d.pos.line - 1),
|
|
89
|
+
...mod.aliases.map((d) => d.pos.line - 1),
|
|
90
|
+
...mod.foreigns.map((d) => d.pos.line - 1),
|
|
91
|
+
...mod.body.bindings.map((b) => (b.sig?.pos ?? b.pos).line - 1),
|
|
92
|
+
...mod.body.bindings.flatMap((b) => b.equations.map((e) => e.pos.line - 1)),
|
|
93
|
+
...(mod.style ? [mod.style.pos.line - 2] : []),
|
|
94
|
+
lines.length,
|
|
95
|
+
].sort((a, b) => a - b);
|
|
96
|
+
return (line) => {
|
|
97
|
+
let next = starts.find((s) => s > line) ?? lines.length;
|
|
98
|
+
// 同じ名前の等式が続くときは、まとめて 1 つの宣言とみなす
|
|
99
|
+
while (next < lines.length && sameBinding(lines[line], lines[next]))
|
|
100
|
+
next = starts.find((s) => s > next) ?? lines.length;
|
|
101
|
+
let end = next - 1;
|
|
102
|
+
while (end > line && /^\s*(--.*)?$/.test(lines[end]))
|
|
103
|
+
end--;
|
|
104
|
+
return end;
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
const KEYWORDS = new Set(["module", "import", "foreign", "data", "type", "style"]);
|
|
108
|
+
function sameBinding(a, b) {
|
|
109
|
+
const name = (l) => /^([a-z_][\w']*)\s/.exec(l)?.[1];
|
|
110
|
+
const na = name(a);
|
|
111
|
+
return na !== undefined && !KEYWORDS.has(na) && na === name(b) && !/^\S+\s+::/.test(b);
|
|
112
|
+
}
|
|
113
|
+
// その行から、字下げがより深い行が続くあいだ(where の中の定義の終わり)
|
|
114
|
+
function blockEnd(lines, line) {
|
|
115
|
+
const indent = lines[line].length - lines[line].trimStart().length;
|
|
116
|
+
let end = line;
|
|
117
|
+
for (let i = line + 1; i < lines.length; i++) {
|
|
118
|
+
if (lines[i].trim() === "")
|
|
119
|
+
continue;
|
|
120
|
+
if (lines[i].length - lines[i].trimStart().length <= indent)
|
|
121
|
+
break;
|
|
122
|
+
end = i;
|
|
123
|
+
}
|
|
124
|
+
return end;
|
|
125
|
+
}
|
|
126
|
+
// JSX の要素(<Tag ...>...</Tag>、<Tag />、<>...</>)の範囲。文字列とコメントは読み飛ばす
|
|
127
|
+
export function jsxElements(text) {
|
|
128
|
+
const spans = [];
|
|
129
|
+
const stack = [];
|
|
130
|
+
let i = 0;
|
|
131
|
+
while (i < text.length) {
|
|
132
|
+
const c = text[i];
|
|
133
|
+
if (c === '"') {
|
|
134
|
+
i++;
|
|
135
|
+
while (i < text.length && text[i] !== '"' && text[i] !== "\n")
|
|
136
|
+
i += text[i] === "\\" ? 2 : 1;
|
|
137
|
+
i++;
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
if (c === "-" && text[i + 1] === "-") {
|
|
141
|
+
while (i < text.length && text[i] !== "\n")
|
|
142
|
+
i++;
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (c === "\n" && text.startsWith("style\n", i + 1))
|
|
146
|
+
break; // style ブロックは CSS
|
|
147
|
+
if (c === "<" && text[i + 1] === "/") {
|
|
148
|
+
const close = text.indexOf(">", i);
|
|
149
|
+
if (close < 0)
|
|
150
|
+
break;
|
|
151
|
+
const start = stack.pop();
|
|
152
|
+
if (start !== undefined)
|
|
153
|
+
spans.push({ start, end: close + 1 });
|
|
154
|
+
i = close + 1;
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
if (c === "<" && /[A-Za-z>]/.test(text[i + 1] ?? "")) {
|
|
158
|
+
const end = tagEnd(text, i);
|
|
159
|
+
if (end < 0)
|
|
160
|
+
break;
|
|
161
|
+
if (text[end - 1] === "/")
|
|
162
|
+
spans.push({ start: i, end: end + 1 });
|
|
163
|
+
else
|
|
164
|
+
stack.push(i);
|
|
165
|
+
i = end + 1;
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
i++;
|
|
169
|
+
}
|
|
170
|
+
return spans;
|
|
171
|
+
}
|
|
172
|
+
// 開きタグの > の位置(属性の {…} と "…" の中の > は数えない)
|
|
173
|
+
function tagEnd(text, from) {
|
|
174
|
+
let depth = 0;
|
|
175
|
+
for (let i = from + 1; i < text.length; i++) {
|
|
176
|
+
const c = text[i];
|
|
177
|
+
if (c === '"') {
|
|
178
|
+
i = text.indexOf('"', i + 1);
|
|
179
|
+
if (i < 0)
|
|
180
|
+
return -1;
|
|
181
|
+
}
|
|
182
|
+
else if (c === "{")
|
|
183
|
+
depth++;
|
|
184
|
+
else if (c === "}")
|
|
185
|
+
depth--;
|
|
186
|
+
else if (c === ">" && depth === 0)
|
|
187
|
+
return i;
|
|
188
|
+
}
|
|
189
|
+
return -1;
|
|
190
|
+
}
|
|
191
|
+
// ---- 折りたたみ ----
|
|
192
|
+
export function foldingRanges(text, mod) {
|
|
193
|
+
const lines = text.split("\n");
|
|
194
|
+
const out = [];
|
|
195
|
+
const add = (startLine, endLine, kind) => {
|
|
196
|
+
if (endLine > startLine)
|
|
197
|
+
out.push({ startLine, endLine, ...(kind ? { kind } : {}) });
|
|
198
|
+
};
|
|
199
|
+
if (mod) {
|
|
200
|
+
const walk = (symbols) => {
|
|
201
|
+
for (const s of symbols) {
|
|
202
|
+
add(s.range.start.line, s.range.end.line);
|
|
203
|
+
if (s.children)
|
|
204
|
+
walk(s.children);
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
walk(outline(text, mod));
|
|
208
|
+
if (mod.imports.length > 1)
|
|
209
|
+
add(mod.imports[0].pos.line - 1, mod.imports[mod.imports.length - 1].pos.line - 1, "imports");
|
|
210
|
+
}
|
|
211
|
+
const lineOf = offsetToLine(text);
|
|
212
|
+
// JSX の要素は、閉じタグの行を残して畳む(<Row ... /> のような自分で閉じる要素は最後の行まで)
|
|
213
|
+
for (const s of jsxElements(text)) {
|
|
214
|
+
const last = lineOf(s.end - 1);
|
|
215
|
+
add(lineOf(s.start), text[s.end - 2] === "/" ? last : last - 1);
|
|
216
|
+
}
|
|
217
|
+
// 続けて書いた -- のコメント(style ブロックの CSS 変数は除く)
|
|
218
|
+
const codeEnd = lines.indexOf("style");
|
|
219
|
+
for (let i = 0; i < (codeEnd < 0 ? lines.length : codeEnd); i++) {
|
|
220
|
+
if (!/^\s*--/.test(lines[i]))
|
|
221
|
+
continue;
|
|
222
|
+
let j = i;
|
|
223
|
+
while (j + 1 < lines.length && /^\s*--/.test(lines[j + 1]))
|
|
224
|
+
j++;
|
|
225
|
+
add(i, j, "comment");
|
|
226
|
+
i = j;
|
|
227
|
+
}
|
|
228
|
+
return out;
|
|
229
|
+
}
|
|
230
|
+
const offsetToLine = (text) => {
|
|
231
|
+
const starts = [0];
|
|
232
|
+
for (let i = 0; i < text.length; i++)
|
|
233
|
+
if (text[i] === "\n")
|
|
234
|
+
starts.push(i + 1);
|
|
235
|
+
return (offset) => {
|
|
236
|
+
let lo = 0;
|
|
237
|
+
let hi = starts.length - 1;
|
|
238
|
+
while (lo < hi) {
|
|
239
|
+
const mid = (lo + hi + 1) >> 1;
|
|
240
|
+
if (starts[mid] <= offset)
|
|
241
|
+
lo = mid;
|
|
242
|
+
else
|
|
243
|
+
hi = mid - 1;
|
|
244
|
+
}
|
|
245
|
+
return lo;
|
|
246
|
+
};
|
|
247
|
+
};
|
|
248
|
+
// 名前 → それを囲む JSX の要素(内側から)→ 等式・定義 → トップレベルの宣言
|
|
249
|
+
export function selectionRange(text, mod, line, character) {
|
|
250
|
+
const lines = text.split("\n");
|
|
251
|
+
const lineStarts = [0];
|
|
252
|
+
for (let i = 0; i < text.length; i++)
|
|
253
|
+
if (text[i] === "\n")
|
|
254
|
+
lineStarts.push(i + 1);
|
|
255
|
+
const offset = (lineStarts[line] ?? text.length) + character;
|
|
256
|
+
const toPos = (o) => {
|
|
257
|
+
let l = lineStarts.length - 1;
|
|
258
|
+
while (lineStarts[l] > o)
|
|
259
|
+
l--;
|
|
260
|
+
return { line: l, character: o - lineStarts[l] };
|
|
261
|
+
};
|
|
262
|
+
const spans = [];
|
|
263
|
+
const word = /[\w'.]*$/.exec(text.slice(0, offset))[0].length;
|
|
264
|
+
const after = /^[\w']*/.exec(text.slice(offset))[0].length;
|
|
265
|
+
if (word + after > 0)
|
|
266
|
+
spans.push({ start: offset - word, end: offset + after });
|
|
267
|
+
const elements = jsxElements(text)
|
|
268
|
+
.filter((s) => s.start <= offset && offset < s.end)
|
|
269
|
+
.sort((a, b) => b.start - a.start || a.end - b.end);
|
|
270
|
+
spans.push(...elements);
|
|
271
|
+
if (mod) {
|
|
272
|
+
const nested = [];
|
|
273
|
+
const walk = (symbols) => {
|
|
274
|
+
for (const s of symbols) {
|
|
275
|
+
if (s.range.start.line <= line && line <= s.range.end.line) {
|
|
276
|
+
nested.unshift(s.range);
|
|
277
|
+
if (s.children)
|
|
278
|
+
walk(s.children);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
walk(outline(text, mod));
|
|
283
|
+
for (const r of nested) {
|
|
284
|
+
spans.push({ start: lineStarts[r.start.line], end: lineStarts[r.end.line] + lines[r.end.line].length });
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
// 外側ほど広くなるよう、広がらないものは除く
|
|
288
|
+
const chain = [];
|
|
289
|
+
for (const s of spans) {
|
|
290
|
+
const prev = chain[chain.length - 1];
|
|
291
|
+
if (!prev || (s.start <= prev.start && s.end >= prev.end && (s.start < prev.start || s.end > prev.end)))
|
|
292
|
+
chain.push(s);
|
|
293
|
+
}
|
|
294
|
+
let result;
|
|
295
|
+
for (const s of chain.reverse())
|
|
296
|
+
result = { range: { start: toPos(s.start), end: toPos(s.end) }, parent: result };
|
|
297
|
+
return result ?? null;
|
|
298
|
+
}
|
package/build/packages.js
CHANGED