@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hatchingpoint/point",
3
- "version": "0.0.10",
3
+ "version": "0.0.11",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Point language compiler and CLI.",
@@ -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: "0.0.9" },
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
  }