@hatchingpoint/point 0.0.10 → 0.0.11
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/package.json +1 -1
- package/src/lsp/analyze.ts +128 -0
- package/src/lsp/server.ts +76 -1
package/package.json
CHANGED
package/src/lsp/analyze.ts
CHANGED
|
@@ -31,6 +31,61 @@ export interface LspHover {
|
|
|
31
31
|
contents: string;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
export interface LspCompletionItem {
|
|
35
|
+
label: string;
|
|
36
|
+
kind: number;
|
|
37
|
+
detail?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const BLOCK_KEYWORDS = [
|
|
41
|
+
"module",
|
|
42
|
+
"use",
|
|
43
|
+
"record",
|
|
44
|
+
"calculation",
|
|
45
|
+
"rule",
|
|
46
|
+
"label",
|
|
47
|
+
"external",
|
|
48
|
+
"action",
|
|
49
|
+
"policy",
|
|
50
|
+
"view",
|
|
51
|
+
"route",
|
|
52
|
+
"workflow",
|
|
53
|
+
"command",
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
const STATEMENT_KEYWORDS = [
|
|
57
|
+
"input",
|
|
58
|
+
"output",
|
|
59
|
+
"return",
|
|
60
|
+
"when",
|
|
61
|
+
"otherwise",
|
|
62
|
+
"add",
|
|
63
|
+
"subtract",
|
|
64
|
+
"set",
|
|
65
|
+
"for",
|
|
66
|
+
"each",
|
|
67
|
+
"in",
|
|
68
|
+
"starts",
|
|
69
|
+
"at",
|
|
70
|
+
"as",
|
|
71
|
+
"to",
|
|
72
|
+
"from",
|
|
73
|
+
"is",
|
|
74
|
+
"render",
|
|
75
|
+
"method",
|
|
76
|
+
"path",
|
|
77
|
+
"step",
|
|
78
|
+
"await",
|
|
79
|
+
"touches",
|
|
80
|
+
"and",
|
|
81
|
+
"or",
|
|
82
|
+
"none",
|
|
83
|
+
"true",
|
|
84
|
+
"false",
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
const TYPE_KEYWORDS = ["Text", "Int", "Float", "Bool", "Void", "List", "Maybe"];
|
|
88
|
+
|
|
34
89
|
export interface PointDocumentAnalysis {
|
|
35
90
|
diagnostics: LspDiagnostic[];
|
|
36
91
|
symbols: PointSemanticSymbol[];
|
|
@@ -128,6 +183,79 @@ export function formatPointDocument(source: string): string {
|
|
|
128
183
|
}
|
|
129
184
|
}
|
|
130
185
|
|
|
186
|
+
export function completionsForPosition(source: string, line: number, column: number): LspCompletionItem[] {
|
|
187
|
+
const analysis = analyzePointSource(source);
|
|
188
|
+
const lines = source.split(/\r?\n/);
|
|
189
|
+
const lineText = lines[line - 1] ?? "";
|
|
190
|
+
const before = lineText.slice(0, Math.max(0, column - 1));
|
|
191
|
+
const items: LspCompletionItem[] = [];
|
|
192
|
+
const seen = new Set<string>();
|
|
193
|
+
const add = (label: string, kind: number, detail?: string) => {
|
|
194
|
+
if (seen.has(label)) return;
|
|
195
|
+
seen.add(label);
|
|
196
|
+
items.push({ label, kind, detail });
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
if (/^\s*$/.test(before)) {
|
|
200
|
+
for (const keyword of BLOCK_KEYWORDS) add(keyword, 14);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
for (const keyword of [...STATEMENT_KEYWORDS, ...TYPE_KEYWORDS]) add(keyword, 14);
|
|
204
|
+
for (const symbol of analysis.symbols) {
|
|
205
|
+
const kind =
|
|
206
|
+
symbol.kind === "field" ? 5 : symbol.kind === "record" ? 7 : symbol.kind === "param" ? 6 : 3;
|
|
207
|
+
add(symbol.name, kind, symbol.kind);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return items;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function renameSymbolInDocument(
|
|
214
|
+
source: string,
|
|
215
|
+
line: number,
|
|
216
|
+
column: number,
|
|
217
|
+
newName: string,
|
|
218
|
+
): { range: LspRange; newText: string } | null {
|
|
219
|
+
const analysis = analyzePointSource(source);
|
|
220
|
+
const symbol = symbolAtPoint(analysis.symbols, line, column);
|
|
221
|
+
if (!symbol?.span || !newName.trim()) return null;
|
|
222
|
+
const oldName = symbol.name;
|
|
223
|
+
if (oldName === newName) {
|
|
224
|
+
return { range: pointSpanToLspRange(symbol.span), newText: oldName };
|
|
225
|
+
}
|
|
226
|
+
const escaped = oldName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
227
|
+
const updated = source.replace(new RegExp(escaped, "g"), newName);
|
|
228
|
+
if (updated === source) return null;
|
|
229
|
+
return {
|
|
230
|
+
range: fullDocumentRange(source),
|
|
231
|
+
newText: updated,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export function prepareRenameAtPosition(
|
|
236
|
+
source: string,
|
|
237
|
+
line: number,
|
|
238
|
+
column: number,
|
|
239
|
+
): { range: LspRange; placeholder: string } | null {
|
|
240
|
+
const analysis = analyzePointSource(source);
|
|
241
|
+
const symbol = symbolAtPoint(analysis.symbols, line, column);
|
|
242
|
+
if (!symbol?.span) return null;
|
|
243
|
+
return {
|
|
244
|
+
range: pointSpanToLspRange(symbol.span),
|
|
245
|
+
placeholder: symbol.name,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function fullDocumentRange(text: string): LspRange {
|
|
250
|
+
const lines = text.split(/\r?\n/);
|
|
251
|
+
const lastLine = Math.max(0, lines.length - 1);
|
|
252
|
+
const lastCharacter = lines[lastLine]?.length ?? 0;
|
|
253
|
+
return {
|
|
254
|
+
start: { line: 0, character: 0 },
|
|
255
|
+
end: { line: lastLine, character: lastCharacter },
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
131
259
|
function toLspDiagnostic(diagnostic: PointCoreDiagnostic): LspDiagnostic {
|
|
132
260
|
const range = diagnostic.span
|
|
133
261
|
? pointSpanToLspRange(diagnostic.span)
|
package/src/lsp/server.ts
CHANGED
|
@@ -1,18 +1,32 @@
|
|
|
1
1
|
import {
|
|
2
2
|
analyzePointSource,
|
|
3
|
+
completionsForPosition,
|
|
3
4
|
definitionForPosition,
|
|
4
5
|
formatPointDocument,
|
|
5
6
|
hoverForPosition,
|
|
6
7
|
lspPositionToPoint,
|
|
7
8
|
outlineSymbols,
|
|
9
|
+
prepareRenameAtPosition,
|
|
10
|
+
renameSymbolInDocument,
|
|
8
11
|
type LspRange,
|
|
9
12
|
} from "./analyze.ts";
|
|
13
|
+
import { readFileSync } from "node:fs";
|
|
14
|
+
import { join } from "node:path";
|
|
10
15
|
import { LspReader, writeMessage, type JsonRpcMessage } from "./protocol.ts";
|
|
11
16
|
|
|
12
17
|
type DocumentState = { version: number; text: string };
|
|
13
18
|
|
|
14
19
|
const documents = new Map<string, DocumentState>();
|
|
15
20
|
|
|
21
|
+
function lspVersion(): string {
|
|
22
|
+
try {
|
|
23
|
+
const pkgPath = join(import.meta.dir, "../../package.json");
|
|
24
|
+
return (JSON.parse(readFileSync(pkgPath, "utf8")) as { version: string }).version;
|
|
25
|
+
} catch {
|
|
26
|
+
return "0.0.0";
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
16
30
|
export async function runPointLspServer(): Promise<void> {
|
|
17
31
|
const reader = new LspReader();
|
|
18
32
|
for await (const message of reader.messages()) {
|
|
@@ -29,8 +43,10 @@ async function handleMessage(message: JsonRpcMessage): Promise<void> {
|
|
|
29
43
|
definitionProvider: true,
|
|
30
44
|
hoverProvider: true,
|
|
31
45
|
documentFormattingProvider: true,
|
|
46
|
+
completionProvider: { triggerCharacters: [".", " "] },
|
|
47
|
+
renameProvider: { prepareProvider: true },
|
|
32
48
|
},
|
|
33
|
-
serverInfo: { name: "point-lsp", version:
|
|
49
|
+
serverInfo: { name: "point-lsp", version: lspVersion() },
|
|
34
50
|
});
|
|
35
51
|
return;
|
|
36
52
|
}
|
|
@@ -130,6 +146,65 @@ async function handleMessage(message: JsonRpcMessage): Promise<void> {
|
|
|
130
146
|
return;
|
|
131
147
|
}
|
|
132
148
|
|
|
149
|
+
if (message.method === "textDocument/completion") {
|
|
150
|
+
const params = message.params as {
|
|
151
|
+
textDocument: { uri: string };
|
|
152
|
+
position: { line: number; character: number };
|
|
153
|
+
};
|
|
154
|
+
const document = documents.get(params.textDocument.uri);
|
|
155
|
+
if (!document) {
|
|
156
|
+
respond(message.id, { isIncomplete: false, items: [] });
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
const point = lspPositionToPoint(params.position.line, params.position.character);
|
|
160
|
+
const items = completionsForPosition(document.text, point.line, point.column);
|
|
161
|
+
respond(message.id, { isIncomplete: false, items });
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (message.method === "textDocument/prepareRename") {
|
|
166
|
+
const params = message.params as {
|
|
167
|
+
textDocument: { uri: string };
|
|
168
|
+
position: { line: number; character: number };
|
|
169
|
+
};
|
|
170
|
+
const document = documents.get(params.textDocument.uri);
|
|
171
|
+
if (!document) {
|
|
172
|
+
respond(message.id, null);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
const point = lspPositionToPoint(params.position.line, params.position.character);
|
|
176
|
+
const prepared = prepareRenameAtPosition(document.text, point.line, point.column);
|
|
177
|
+
respond(message.id, prepared);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (message.method === "textDocument/rename") {
|
|
182
|
+
const params = message.params as {
|
|
183
|
+
textDocument: { uri: string };
|
|
184
|
+
position: { line: number; character: number };
|
|
185
|
+
newName: string;
|
|
186
|
+
};
|
|
187
|
+
const document = documents.get(params.textDocument.uri);
|
|
188
|
+
if (!document) {
|
|
189
|
+
respond(message.id, null);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
const point = lspPositionToPoint(params.position.line, params.position.character);
|
|
193
|
+
const edit = renameSymbolInDocument(document.text, point.line, point.column, params.newName);
|
|
194
|
+
if (!edit) {
|
|
195
|
+
respond(message.id, null);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
documents.set(params.textDocument.uri, { version: document.version, text: edit.newText });
|
|
199
|
+
publishDiagnostics(params.textDocument.uri);
|
|
200
|
+
respond(message.id, {
|
|
201
|
+
changes: {
|
|
202
|
+
[params.textDocument.uri]: [edit],
|
|
203
|
+
},
|
|
204
|
+
});
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
133
208
|
if (message.id !== undefined) {
|
|
134
209
|
respond(message.id, null);
|
|
135
210
|
}
|