@hatchingpoint/point 0.0.10 → 0.0.12
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 +2 -2
- package/package.json +1 -1
- package/src/core/check-docs.ts +155 -0
- package/src/core/cli.ts +53 -23
- package/src/core/emit-javascript.ts +62 -0
- package/src/core/emit-python.ts +173 -0
- package/src/core/emit-typescript.ts +5 -2
- package/src/core/index.ts +2 -0
- package/src/lsp/analyze.ts +128 -0
- package/src/lsp/server.ts +76 -1
- package/src/semantic/expressions.ts +20 -3
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ point check examples/math.point
|
|
|
13
13
|
|
|
14
14
|
Pair with the [Point Language](https://marketplace.visualstudio.com/items?itemName=hatchingpoint.point) extension in VS Code or Cursor.
|
|
15
15
|
|
|
16
|
-
Point's public source language is semantic product logic. The compiler lowers that source into an internal typed core and emits TypeScript for existing JavaScript infrastructure.
|
|
16
|
+
Point's public source language is semantic product logic. The compiler lowers that source into an internal typed core and emits JavaScript by default for Bun and Node. Use `point build-ts` when you need TypeScript for existing typed JavaScript infrastructure.
|
|
17
17
|
|
|
18
18
|
This package is the source of truth for Point. It exposes:
|
|
19
19
|
|
|
@@ -29,6 +29,6 @@ bun run check
|
|
|
29
29
|
bun run build
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
-
`bun run build` emits
|
|
32
|
+
`bun run build` emits JavaScript into `generated/` by default. Use `bun run build:ts` for TypeScript and `bun run build:ast` when debugging compiler output.
|
|
33
33
|
|
|
34
34
|
When Point is extracted, this package can move into a standalone repo with the same package name and public entrypoints.
|
package/package.json
CHANGED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
3
|
+
import type { PointCoreDiagnostic } from "./check.ts";
|
|
4
|
+
import { checkPointCore } from "./check.ts";
|
|
5
|
+
import { parsePointSource } from "./parser.ts";
|
|
6
|
+
|
|
7
|
+
const DEFAULT_DOCS_DIR = "docs/site";
|
|
8
|
+
const POINT_FENCE = /```point\r?\n([\s\S]*?)```/g;
|
|
9
|
+
const POINT_FILE_REF = /\b(?:[\w.-]+\/)*[\w.-]+\.point\b/g;
|
|
10
|
+
|
|
11
|
+
export interface DocsCheckItemResult {
|
|
12
|
+
kind: "snippet" | "file";
|
|
13
|
+
source: string;
|
|
14
|
+
label: string;
|
|
15
|
+
line?: number;
|
|
16
|
+
ok: boolean;
|
|
17
|
+
diagnostics: PointCoreDiagnostic[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface DocsCheckResult {
|
|
21
|
+
ok: boolean;
|
|
22
|
+
checked: number;
|
|
23
|
+
items: DocsCheckItemResult[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function discoverDocsSiteMarkdown(docsDir = DEFAULT_DOCS_DIR, cwd = process.cwd()): Promise<string[]> {
|
|
27
|
+
const root = resolve(cwd, docsDir);
|
|
28
|
+
const glob = new Bun.Glob("**/*.md");
|
|
29
|
+
const files: string[] = [];
|
|
30
|
+
for await (const file of glob.scan({ cwd: root, onlyFiles: true })) {
|
|
31
|
+
files.push(`${docsDir}/${file.replaceAll("\\", "/")}`);
|
|
32
|
+
}
|
|
33
|
+
return files.sort((a, b) => a.localeCompare(b));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function extractPointSnippets(markdown: string, source: string): Array<{ label: string; code: string; line: number }> {
|
|
37
|
+
const snippets: Array<{ label: string; code: string; line: number }> = [];
|
|
38
|
+
let index = 0;
|
|
39
|
+
for (const match of markdown.matchAll(POINT_FENCE)) {
|
|
40
|
+
index += 1;
|
|
41
|
+
const code = match[1]?.replace(/\s+$/, "") ?? "";
|
|
42
|
+
const line = markdown.slice(0, match.index ?? 0).split(/\r?\n/).length;
|
|
43
|
+
snippets.push({ label: `${source} snippet ${index}`, code, line });
|
|
44
|
+
}
|
|
45
|
+
return snippets;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function extractPointFileReferences(markdown: string, markdownPath: string, cwd = process.cwd()): string[] {
|
|
49
|
+
const references = new Set<string>();
|
|
50
|
+
for (const match of markdown.matchAll(POINT_FILE_REF)) {
|
|
51
|
+
const candidate = match[0]!;
|
|
52
|
+
if (candidate.endsWith(".point")) references.add(candidate.replaceAll("\\", "/"));
|
|
53
|
+
}
|
|
54
|
+
const resolved: string[] = [];
|
|
55
|
+
for (const reference of references) {
|
|
56
|
+
const absolute = resolvePointFileReference(reference, markdownPath, cwd);
|
|
57
|
+
if (absolute) resolved.push(absolute);
|
|
58
|
+
}
|
|
59
|
+
return resolved.sort((a, b) => a.localeCompare(b));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function resolvePointFileReference(reference: string, markdownPath: string, cwd: string): string | null {
|
|
63
|
+
const candidates = [
|
|
64
|
+
resolve(cwd, reference),
|
|
65
|
+
resolve(cwd, dirname(markdownPath), reference),
|
|
66
|
+
];
|
|
67
|
+
for (const candidate of candidates) {
|
|
68
|
+
if (!existsSync(candidate)) continue;
|
|
69
|
+
return candidate.replace(resolve(cwd), "").replace(/^[/\\]/, "").replaceAll("\\", "/");
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function checkPointSource(source: string, label: string, kind: "snippet" | "file", markdownSource: string, line?: number): DocsCheckItemResult {
|
|
75
|
+
try {
|
|
76
|
+
const program = parsePointSource(source);
|
|
77
|
+
const diagnostics = checkPointCore(program);
|
|
78
|
+
return { kind, source: markdownSource, label, line, ok: diagnostics.length === 0, diagnostics };
|
|
79
|
+
} catch (error) {
|
|
80
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
81
|
+
return {
|
|
82
|
+
kind,
|
|
83
|
+
source: markdownSource,
|
|
84
|
+
label,
|
|
85
|
+
line,
|
|
86
|
+
ok: false,
|
|
87
|
+
diagnostics: [
|
|
88
|
+
{
|
|
89
|
+
code: "parse-error",
|
|
90
|
+
message,
|
|
91
|
+
path: label,
|
|
92
|
+
ref: `point://docs/${label}`,
|
|
93
|
+
severity: "error",
|
|
94
|
+
span: null,
|
|
95
|
+
repair: "Fix the Point syntax in this docs snippet or referenced file.",
|
|
96
|
+
},
|
|
97
|
+
],
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export async function checkDocs(options: { docsDir?: string; cwd?: string } = {}): Promise<DocsCheckResult> {
|
|
103
|
+
const docsDir = options.docsDir ?? DEFAULT_DOCS_DIR;
|
|
104
|
+
const cwd = options.cwd ?? process.cwd();
|
|
105
|
+
const markdownFiles = await discoverDocsSiteMarkdown(docsDir, cwd);
|
|
106
|
+
const items: DocsCheckItemResult[] = [];
|
|
107
|
+
const checkedFiles = new Set<string>();
|
|
108
|
+
|
|
109
|
+
for (const markdownPath of markdownFiles) {
|
|
110
|
+
const absoluteMarkdownPath = resolve(cwd, markdownPath);
|
|
111
|
+
const markdown = await Bun.file(absoluteMarkdownPath).text();
|
|
112
|
+
|
|
113
|
+
for (const snippet of extractPointSnippets(markdown, markdownPath)) {
|
|
114
|
+
items.push(checkPointSource(snippet.code, snippet.label, "snippet", markdownPath, snippet.line));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
for (const filePath of extractPointFileReferences(markdown, markdownPath, cwd)) {
|
|
118
|
+
if (checkedFiles.has(filePath)) continue;
|
|
119
|
+
checkedFiles.add(filePath);
|
|
120
|
+
const source = await Bun.file(resolve(cwd, filePath)).text();
|
|
121
|
+
items.push(checkPointSource(source, filePath, "file", markdownPath));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return { ok: items.every((item) => item.ok), checked: items.length, items };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export async function runCheckDocs(options: { docsDir?: string; cwd?: string } = {}): Promise<DocsCheckResult> {
|
|
129
|
+
const result = await checkDocs(options);
|
|
130
|
+
if (result.ok) {
|
|
131
|
+
const snippets = result.items.filter((item) => item.kind === "snippet").length;
|
|
132
|
+
const files = result.items.filter((item) => item.kind === "file").length;
|
|
133
|
+
console.log(`Point docs check passed: ${snippets} snippet(s), ${files} file reference(s)`);
|
|
134
|
+
return result;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const failures = result.items.filter((item) => !item.ok);
|
|
138
|
+
console.error(
|
|
139
|
+
JSON.stringify(
|
|
140
|
+
{
|
|
141
|
+
ok: false,
|
|
142
|
+
failures: failures.map((item) => ({
|
|
143
|
+
kind: item.kind,
|
|
144
|
+
source: item.source,
|
|
145
|
+
label: item.label,
|
|
146
|
+
line: item.line,
|
|
147
|
+
diagnostics: item.diagnostics,
|
|
148
|
+
})),
|
|
149
|
+
},
|
|
150
|
+
null,
|
|
151
|
+
2,
|
|
152
|
+
),
|
|
153
|
+
);
|
|
154
|
+
process.exit(1);
|
|
155
|
+
}
|
package/src/core/cli.ts
CHANGED
|
@@ -6,14 +6,18 @@ import { createPointCoreIndex, createPointCoreRepairPlan, explainPointCoreRef }
|
|
|
6
6
|
import { createSemanticIndex, explainSemanticRef, mapPublicDiagnostics } from "../semantic/context.ts";
|
|
7
7
|
import { emitPointCoreTypeScript } from "./emit-typescript.ts";
|
|
8
8
|
import { emitPointCoreJavaScript } from "./emit-javascript.ts";
|
|
9
|
+
import { emitPointCorePython } from "./emit-python.ts";
|
|
9
10
|
import { formatPointSource } from "./format.ts";
|
|
10
11
|
import { isCacheHit, isIncrementalEnabled, readBuildCache, recordCacheEntry, writeBuildCache } from "./incremental.ts";
|
|
11
12
|
import { parsePointSource } from "./parser.ts";
|
|
13
|
+
import { runCheckDocs } from "./check-docs.ts";
|
|
12
14
|
import { runPointLspServer } from "../lsp/server.ts";
|
|
13
15
|
|
|
14
16
|
const DEFAULT_INPUT = "examples/math.point";
|
|
15
17
|
const DEFAULT_OUTPUT = "generated/math.ast.json";
|
|
18
|
+
const DEFAULT_JS_OUTPUT = "generated/math.js";
|
|
16
19
|
const DEFAULT_TS_OUTPUT = "generated/math.ts";
|
|
20
|
+
const DEFAULT_PY_OUTPUT = "generated/math.py";
|
|
17
21
|
const DEFAULT_PATTERNS = ["examples/**/*.point", "std/**/*.point", "compiler/**/*.point"];
|
|
18
22
|
const GENERATED_DIR = "generated";
|
|
19
23
|
|
|
@@ -34,6 +38,11 @@ export async function main() {
|
|
|
34
38
|
return;
|
|
35
39
|
}
|
|
36
40
|
|
|
41
|
+
if (command === "check-docs") {
|
|
42
|
+
await runCheckDocs();
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
37
46
|
const inputPath = resolve(process.cwd(), input);
|
|
38
47
|
const source = await Bun.file(inputPath).text();
|
|
39
48
|
const program = parsePointSource(source);
|
|
@@ -101,15 +110,27 @@ export async function main() {
|
|
|
101
110
|
return;
|
|
102
111
|
}
|
|
103
112
|
|
|
104
|
-
if (command === "build") {
|
|
113
|
+
if (command === "build" || command === "build-js") {
|
|
114
|
+
if (diagnostics.length > 0) {
|
|
115
|
+
console.error(JSON.stringify({ ok: false, diagnostics }, null, 2));
|
|
116
|
+
process.exit(1);
|
|
117
|
+
}
|
|
118
|
+
const outputPath = resolve(process.cwd(), output === DEFAULT_OUTPUT ? DEFAULT_JS_OUTPUT : output);
|
|
119
|
+
await Bun.$`mkdir -p ${dirname(outputPath)}`.quiet();
|
|
120
|
+
await Bun.write(outputPath, emitPointCoreJavaScript(program));
|
|
121
|
+
console.log(`Point core JavaScript build wrote ${outputPath.replaceAll("\\", "/")}`);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (command === "build-ast") {
|
|
105
126
|
if (diagnostics.length > 0) {
|
|
106
127
|
console.error(JSON.stringify({ ok: false, diagnostics }, null, 2));
|
|
107
128
|
process.exit(1);
|
|
108
129
|
}
|
|
109
|
-
const outputPath = resolve(process.cwd(), output);
|
|
130
|
+
const outputPath = resolve(process.cwd(), output === DEFAULT_OUTPUT ? DEFAULT_OUTPUT : output);
|
|
110
131
|
await Bun.$`mkdir -p ${dirname(outputPath)}`.quiet();
|
|
111
132
|
await Bun.write(outputPath, `${JSON.stringify(program, null, 2)}\n`);
|
|
112
|
-
console.log(`Point core build wrote ${
|
|
133
|
+
console.log(`Point core AST build wrote ${outputPath.replaceAll("\\", "/")}`);
|
|
113
134
|
return;
|
|
114
135
|
}
|
|
115
136
|
|
|
@@ -125,15 +146,15 @@ export async function main() {
|
|
|
125
146
|
return;
|
|
126
147
|
}
|
|
127
148
|
|
|
128
|
-
if (command === "build-
|
|
149
|
+
if (command === "build-py") {
|
|
129
150
|
if (diagnostics.length > 0) {
|
|
130
151
|
console.error(JSON.stringify({ ok: false, diagnostics }, null, 2));
|
|
131
152
|
process.exit(1);
|
|
132
153
|
}
|
|
133
|
-
const outputPath = resolve(process.cwd(), output === DEFAULT_OUTPUT ?
|
|
154
|
+
const outputPath = resolve(process.cwd(), output === DEFAULT_OUTPUT ? DEFAULT_PY_OUTPUT : output);
|
|
134
155
|
await Bun.$`mkdir -p ${dirname(outputPath)}`.quiet();
|
|
135
|
-
await Bun.write(outputPath,
|
|
136
|
-
console.log(`Point core
|
|
156
|
+
await Bun.write(outputPath, emitPointCorePython(program));
|
|
157
|
+
console.log(`Point core Python build wrote ${outputPath.replaceAll("\\", "/")}`);
|
|
137
158
|
return;
|
|
138
159
|
}
|
|
139
160
|
|
|
@@ -142,8 +163,8 @@ export async function main() {
|
|
|
142
163
|
console.error(JSON.stringify({ ok: false, diagnostics }, null, 2));
|
|
143
164
|
process.exit(1);
|
|
144
165
|
}
|
|
145
|
-
const runOutput = resolve(tmpdir(), `point-run-${Date.now()}.
|
|
146
|
-
await Bun.write(runOutput,
|
|
166
|
+
const runOutput = resolve(tmpdir(), `point-run-${Date.now()}.js`);
|
|
167
|
+
await Bun.write(runOutput, emitPointCoreJavaScript(program));
|
|
147
168
|
let entryName: string | null = null;
|
|
148
169
|
try {
|
|
149
170
|
const mod = await import(pathToFileUrl(runOutput));
|
|
@@ -223,7 +244,7 @@ async function runProjectCommand(command: string) {
|
|
|
223
244
|
return;
|
|
224
245
|
}
|
|
225
246
|
|
|
226
|
-
if (command === "build-all") {
|
|
247
|
+
if (command === "build-all" || command === "build-js-all") {
|
|
227
248
|
const diagnostics = orderedResults.flatMap((result) =>
|
|
228
249
|
checkPointCore(programWithDependencyDeclarations(result, graph)).map((diagnostic) => ({ ...diagnostic, file: result.input })),
|
|
229
250
|
);
|
|
@@ -232,16 +253,16 @@ async function runProjectCommand(command: string) {
|
|
|
232
253
|
process.exit(1);
|
|
233
254
|
}
|
|
234
255
|
for (const result of orderedResults) {
|
|
235
|
-
const output =
|
|
256
|
+
const output = jsOutputFor(result.input);
|
|
236
257
|
const outputPath = resolve(process.cwd(), output);
|
|
237
258
|
await Bun.$`mkdir -p ${dirname(outputPath)}`.quiet();
|
|
238
|
-
await Bun.write(outputPath,
|
|
259
|
+
await Bun.write(outputPath, emitPointCoreJavaScript(programWithTypeScriptImports(result, graph)));
|
|
239
260
|
}
|
|
240
|
-
console.log(`Point core build wrote ${results.length} files`);
|
|
261
|
+
console.log(`Point core JavaScript build wrote ${results.length} files`);
|
|
241
262
|
return;
|
|
242
263
|
}
|
|
243
264
|
|
|
244
|
-
if (command === "build-
|
|
265
|
+
if (command === "build-ast-all") {
|
|
245
266
|
const diagnostics = orderedResults.flatMap((result) =>
|
|
246
267
|
checkPointCore(programWithDependencyDeclarations(result, graph)).map((diagnostic) => ({ ...diagnostic, file: result.input })),
|
|
247
268
|
);
|
|
@@ -250,16 +271,16 @@ async function runProjectCommand(command: string) {
|
|
|
250
271
|
process.exit(1);
|
|
251
272
|
}
|
|
252
273
|
for (const result of orderedResults) {
|
|
253
|
-
const output =
|
|
274
|
+
const output = astOutputFor(result.input);
|
|
254
275
|
const outputPath = resolve(process.cwd(), output);
|
|
255
276
|
await Bun.$`mkdir -p ${dirname(outputPath)}`.quiet();
|
|
256
|
-
await Bun.write(outputPath,
|
|
277
|
+
await Bun.write(outputPath, `${JSON.stringify(result.program, null, 2)}\n`);
|
|
257
278
|
}
|
|
258
|
-
console.log(`Point core
|
|
279
|
+
console.log(`Point core AST build wrote ${results.length} files`);
|
|
259
280
|
return;
|
|
260
281
|
}
|
|
261
282
|
|
|
262
|
-
if (command === "build-
|
|
283
|
+
if (command === "build-ts-all") {
|
|
263
284
|
const diagnostics = orderedResults.flatMap((result) =>
|
|
264
285
|
checkPointCore(programWithDependencyDeclarations(result, graph)).map((diagnostic) => ({ ...diagnostic, file: result.input })),
|
|
265
286
|
);
|
|
@@ -268,12 +289,12 @@ async function runProjectCommand(command: string) {
|
|
|
268
289
|
process.exit(1);
|
|
269
290
|
}
|
|
270
291
|
for (const result of orderedResults) {
|
|
271
|
-
const output =
|
|
292
|
+
const output = tsOutputFor(result.input);
|
|
272
293
|
const outputPath = resolve(process.cwd(), output);
|
|
273
294
|
await Bun.$`mkdir -p ${dirname(outputPath)}`.quiet();
|
|
274
|
-
await Bun.write(outputPath,
|
|
295
|
+
await Bun.write(outputPath, emitPointCoreTypeScript(programWithTypeScriptImports(result, graph)));
|
|
275
296
|
}
|
|
276
|
-
console.log(`Point core
|
|
297
|
+
console.log(`Point core TypeScript build wrote ${results.length} files`);
|
|
277
298
|
return;
|
|
278
299
|
}
|
|
279
300
|
|
|
@@ -358,8 +379,8 @@ async function runPointTests(program: PointCoreProgram, input: string): Promise<
|
|
|
358
379
|
(declaration.semantic?.name.startsWith("test") || declaration.name.startsWith("test")),
|
|
359
380
|
);
|
|
360
381
|
if (tests.length === 0) return { file: input, ok: true, tests: [] };
|
|
361
|
-
const testOutput = resolve(tmpdir(), `point-test-${Date.now()}-${Math.random().toString(16).slice(2)}.
|
|
362
|
-
await Bun.write(testOutput,
|
|
382
|
+
const testOutput = resolve(tmpdir(), `point-test-${Date.now()}-${Math.random().toString(16).slice(2)}.js`);
|
|
383
|
+
await Bun.write(testOutput, emitPointCoreJavaScript(program));
|
|
363
384
|
const mod = await import(pathToFileUrl(testOutput));
|
|
364
385
|
const results = [];
|
|
365
386
|
for (const test of tests) {
|
|
@@ -498,6 +519,15 @@ function jsOutputFor(input: string): string {
|
|
|
498
519
|
return `${GENERATED_DIR}/${name}.js`;
|
|
499
520
|
}
|
|
500
521
|
|
|
522
|
+
function astOutputFor(input: string): string {
|
|
523
|
+
return outputFor(input);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function pyOutputFor(input: string): string {
|
|
527
|
+
const name = outputBaseName(input);
|
|
528
|
+
return `${GENERATED_DIR}/${name}.py`;
|
|
529
|
+
}
|
|
530
|
+
|
|
501
531
|
function outputBaseName(input: string): string {
|
|
502
532
|
return normalizeInput(input).split("/").pop()?.replace(/\.point$/, "") ?? "program";
|
|
503
533
|
}
|
|
@@ -8,6 +8,8 @@ import type {
|
|
|
8
8
|
PointCoreTypeDeclaration,
|
|
9
9
|
PointCoreValueDeclaration,
|
|
10
10
|
} from "./ast.ts";
|
|
11
|
+
import type { PointSemanticRouteDeclaration } from "../semantic/ast.ts";
|
|
12
|
+
import { semanticFunctionName } from "../semantic/naming.ts";
|
|
11
13
|
|
|
12
14
|
const BINARY_OPERATORS: Record<string, string> = {
|
|
13
15
|
and: "&&",
|
|
@@ -16,13 +18,22 @@ const BINARY_OPERATORS: Record<string, string> = {
|
|
|
16
18
|
|
|
17
19
|
/** Emit JavaScript from a core AST program (no type syntax). Production path: parsePointSource → check → emit. */
|
|
18
20
|
export function emitPointCoreJavaScript(program: PointCoreProgram): string {
|
|
21
|
+
const routes = program.semanticSource?.declarations.filter((declaration): declaration is PointSemanticRouteDeclaration => declaration.kind === "route") ?? [];
|
|
22
|
+
const routeServeCommand = program.declarations.find((declaration) => declaration.kind === "function" && isRouteServeCommand(declaration));
|
|
19
23
|
const lines: string[] = [];
|
|
20
24
|
lines.push("// Generated by Point. Do not edit directly.");
|
|
21
25
|
if (program.module) lines.push(`// Point module: ${program.module}`);
|
|
22
26
|
lines.push("");
|
|
23
27
|
for (const declaration of program.declarations) {
|
|
28
|
+
if (declaration.kind === "function" && declaration.semantic?.kind === "command" && routes.length > 0 && isRouteServeCommand(declaration)) {
|
|
29
|
+
lines.push(...emitRouteServeCommand(declaration), "");
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
24
32
|
lines.push(...emitDeclaration(declaration), "");
|
|
25
33
|
}
|
|
34
|
+
if (routes.length > 0 && routeServeCommand) {
|
|
35
|
+
lines.push(...emitRouteServerRuntime(routes), "");
|
|
36
|
+
}
|
|
26
37
|
return `${trimTrailingBlankLines(lines).join("\n")}\n`;
|
|
27
38
|
}
|
|
28
39
|
|
|
@@ -122,3 +133,54 @@ function trimTrailingBlankLines(lines: string[]): string[] {
|
|
|
122
133
|
while (lines.at(-1) === "") lines.pop();
|
|
123
134
|
return lines;
|
|
124
135
|
}
|
|
136
|
+
|
|
137
|
+
function isRouteServeCommand(declaration: PointCoreFunctionDeclaration): boolean {
|
|
138
|
+
return declaration.semantic?.name === "serve store readiness" || declaration.name === "serveStoreReadinessCommand";
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function emitRouteServeCommand(declaration: PointCoreFunctionDeclaration): string[] {
|
|
142
|
+
const asyncPrefix = declaration.semantic?.kind === "command" ? "async " : "";
|
|
143
|
+
return [
|
|
144
|
+
`export ${asyncPrefix}function ${declaration.name}(${declaration.params.map(emitParam).join(", ")}) {`,
|
|
145
|
+
" const port = Number(process.env.PORT ?? 3456);",
|
|
146
|
+
" const server = Bun.serve({ port, fetch: createPointRouteFetchHandler() });",
|
|
147
|
+
" console.log(`Store readiness listening on http://localhost:${server.port}`);",
|
|
148
|
+
" await new Promise(() => {});",
|
|
149
|
+
"}",
|
|
150
|
+
];
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function emitRouteServerRuntime(routes: PointSemanticRouteDeclaration[]): string[] {
|
|
154
|
+
const matchers = routes.map((route) => {
|
|
155
|
+
const handlerName = semanticFunctionName(route.name, "route", "route");
|
|
156
|
+
const pattern = route.path.replace(/:[A-Za-z][A-Za-z0-9_]*/g, "([^/]+)").replace(/\//g, "\\/");
|
|
157
|
+
const argExpressions = route.inputs.map((_input, index) => `match[${index + 1}]`);
|
|
158
|
+
return {
|
|
159
|
+
method: route.method.toUpperCase(),
|
|
160
|
+
pattern,
|
|
161
|
+
handlerName,
|
|
162
|
+
argExpressions,
|
|
163
|
+
};
|
|
164
|
+
});
|
|
165
|
+
const matchLines = matchers.flatMap((matcher) => [
|
|
166
|
+
` if (req.method === ${JSON.stringify(matcher.method)} && new RegExp(${JSON.stringify(`^${matcher.pattern}$`)}).test(url.pathname)) {`,
|
|
167
|
+
` const match = url.pathname.match(new RegExp(${JSON.stringify(`^${matcher.pattern}$`)}));`,
|
|
168
|
+
` const body = ${matcher.handlerName}(${matcher.argExpressions.join(", ")});`,
|
|
169
|
+
' return new Response(typeof body === "string" ? body : body, { headers: { "content-type": "application/json" } });',
|
|
170
|
+
" }",
|
|
171
|
+
]);
|
|
172
|
+
return [
|
|
173
|
+
"export function createPointRouteFetchHandler() {",
|
|
174
|
+
" return async (req) => {",
|
|
175
|
+
" const url = new URL(req.url);",
|
|
176
|
+
...matchLines,
|
|
177
|
+
' return new Response(JSON.stringify({ error: "Not found" }), { status: 404, headers: { "content-type": "application/json" } });',
|
|
178
|
+
" };",
|
|
179
|
+
"}",
|
|
180
|
+
"",
|
|
181
|
+
"export function startRoutesServer() {",
|
|
182
|
+
" const port = Number(process.env.PORT ?? 3456);",
|
|
183
|
+
" return Bun.serve({ port, fetch: createPointRouteFetchHandler() });",
|
|
184
|
+
"}",
|
|
185
|
+
];
|
|
186
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
PointCoreDeclaration,
|
|
3
|
+
PointCoreExpression,
|
|
4
|
+
PointCoreFunctionDeclaration,
|
|
5
|
+
PointCoreParameter,
|
|
6
|
+
PointCorePrimitiveType,
|
|
7
|
+
PointCoreProgram,
|
|
8
|
+
PointCoreStatement,
|
|
9
|
+
PointCoreTypeDeclaration,
|
|
10
|
+
PointCoreTypeExpression,
|
|
11
|
+
PointCoreValueDeclaration,
|
|
12
|
+
} from "./ast.ts";
|
|
13
|
+
|
|
14
|
+
const BINARY_OPERATORS: Record<string, string> = {
|
|
15
|
+
and: "and",
|
|
16
|
+
or: "or",
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const UNSUPPORTED_SEMANTIC_KINDS = new Set(["view", "route", "action", "workflow", "command"]);
|
|
20
|
+
|
|
21
|
+
/** Emit Python from a core AST program (pure logic modules). Production path: parsePointSource → check → emit. */
|
|
22
|
+
export function emitPointCorePython(program: PointCoreProgram): string {
|
|
23
|
+
const lines: string[] = [];
|
|
24
|
+
lines.push("# Generated by Point. Do not edit directly.");
|
|
25
|
+
if (program.module) lines.push(`# Point module: ${program.module}`);
|
|
26
|
+
lines.push("");
|
|
27
|
+
lines.push("from __future__ import annotations");
|
|
28
|
+
lines.push("");
|
|
29
|
+
if (program.declarations.some((declaration) => declaration.kind === "type")) {
|
|
30
|
+
lines.push("from typing import TypedDict");
|
|
31
|
+
lines.push("");
|
|
32
|
+
}
|
|
33
|
+
for (const declaration of program.declarations) {
|
|
34
|
+
const emitted = emitDeclaration(declaration);
|
|
35
|
+
if (emitted.length > 0) lines.push(...emitted, "");
|
|
36
|
+
}
|
|
37
|
+
return `${trimTrailingBlankLines(lines).join("\n")}\n`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function emitDeclaration(declaration: PointCoreDeclaration): string[] {
|
|
41
|
+
if (declaration.kind === "import") {
|
|
42
|
+
const moduleName = declaration.from.replace(/^\.\//, "").replace(/-/g, "_");
|
|
43
|
+
return [`from ${toPythonModuleName(moduleName)} import ${declaration.names.join(", ")}`];
|
|
44
|
+
}
|
|
45
|
+
if (declaration.kind === "external") {
|
|
46
|
+
const moduleName = declaration.from.replace(/^\.\//, "").replace(/-/g, "_");
|
|
47
|
+
const imported = declaration.importName ?? declaration.name;
|
|
48
|
+
return [`from ${toPythonModuleName(moduleName)} import ${imported} as ${declaration.name}`];
|
|
49
|
+
}
|
|
50
|
+
if (declaration.kind === "type") return emitType(declaration);
|
|
51
|
+
if (declaration.kind === "value") return [emitValue(declaration)];
|
|
52
|
+
if (declaration.semantic && UNSUPPORTED_SEMANTIC_KINDS.has(declaration.semantic.kind)) {
|
|
53
|
+
return [`# Point: ${declaration.semantic.kind} blocks are not supported in Python emit yet`];
|
|
54
|
+
}
|
|
55
|
+
return emitFunction(declaration);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function emitType(declaration: PointCoreTypeDeclaration): string[] {
|
|
59
|
+
return [
|
|
60
|
+
`class ${declaration.name}(TypedDict):`,
|
|
61
|
+
...declaration.fields.map((field) => ` ${field.name}: ${emitTypeExpression(field.type)}`),
|
|
62
|
+
];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function emitFunction(declaration: PointCoreFunctionDeclaration): string[] {
|
|
66
|
+
const asyncPrefix = declaration.semantic?.kind === "action" || declaration.semantic?.kind === "workflow" || declaration.semantic?.kind === "command" ? "async " : "";
|
|
67
|
+
return [
|
|
68
|
+
`${asyncPrefix}def ${declaration.name}(${declaration.params.map(emitParam).join(", ")}) -> ${emitReturnType(declaration)}:`,
|
|
69
|
+
...indentLines(declaration.body.flatMap((statement) => emitStatement(statement, declaration.semantic?.kind))),
|
|
70
|
+
];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function emitReturnType(declaration: PointCoreFunctionDeclaration): string {
|
|
74
|
+
if (declaration.semantic?.kind === "action" || declaration.semantic?.kind === "workflow" || declaration.semantic?.kind === "command") {
|
|
75
|
+
return emitTypeExpression(declaration.returnType);
|
|
76
|
+
}
|
|
77
|
+
return emitTypeExpression(declaration.returnType);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function emitStatement(statement: PointCoreStatement, semanticKind?: string): string[] {
|
|
81
|
+
if (statement.kind === "return") {
|
|
82
|
+
if (semanticKind === "view") return ["# Point: view return values are not supported in Python emit yet", "return \"\""];
|
|
83
|
+
return [statement.value ? `return ${emitExpression(statement.value)}` : "return"];
|
|
84
|
+
}
|
|
85
|
+
if (statement.kind === "value") return [emitValue(statement)];
|
|
86
|
+
if (statement.kind === "assignment") return [`${statement.name} ${statement.operator} ${emitExpression(statement.value)}`];
|
|
87
|
+
if (statement.kind === "if") {
|
|
88
|
+
const lines = [`if ${emitCondition(statement.condition)}:`, ...indentLines(statement.thenBody.flatMap((child) => emitStatement(child, semanticKind)))];
|
|
89
|
+
if (statement.elseBody.length > 0) {
|
|
90
|
+
lines.push("else:", ...indentLines(statement.elseBody.flatMap((child) => emitStatement(child, semanticKind))));
|
|
91
|
+
}
|
|
92
|
+
return lines;
|
|
93
|
+
}
|
|
94
|
+
if (statement.kind === "for") {
|
|
95
|
+
return [`for ${statement.itemName} in ${emitExpression(statement.iterable)}:`, ...indentLines(statement.body.flatMap((child) => emitStatement(child, semanticKind)))];
|
|
96
|
+
}
|
|
97
|
+
return [emitExpression(statement.value)];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function emitValue(declaration: PointCoreValueDeclaration): string {
|
|
101
|
+
return `${declaration.name}: ${emitTypeExpression(declaration.type)} = ${emitExpression(declaration.value)}`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function emitParam(param: PointCoreParameter): string {
|
|
105
|
+
return `${param.name}: ${emitTypeExpression(param.type)}`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function emitTypeExpression(type: PointCoreTypeExpression): string {
|
|
109
|
+
if (type.name === "List") return `list[${type.args[0] ? emitTypeExpression(type.args[0]) : "object"}]`;
|
|
110
|
+
if (type.name === "Maybe") return `${type.args[0] ? emitTypeExpression(type.args[0]) : "object"} | None`;
|
|
111
|
+
if (type.name === "Or") return type.args.map(emitTypeExpression).join(" | ");
|
|
112
|
+
if (type.name === "Error") return "dict[str, str]";
|
|
113
|
+
if (isPrimitiveType(type.name)) return emitPrimitiveType(type.name);
|
|
114
|
+
return type.name;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function emitPrimitiveType(type: PointCorePrimitiveType): string {
|
|
118
|
+
if (type === "Text") return "str";
|
|
119
|
+
if (type === "Int") return "int";
|
|
120
|
+
if (type === "Float") return "float";
|
|
121
|
+
if (type === "Bool") return "bool";
|
|
122
|
+
return "None";
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function emitExpression(expression: PointCoreExpression): string {
|
|
126
|
+
if (expression.kind === "literal") return emitLiteral(expression.value);
|
|
127
|
+
if (expression.kind === "identifier") return expression.name;
|
|
128
|
+
if (expression.kind === "list") return `[${expression.items.map(emitExpression).join(", ")}]`;
|
|
129
|
+
if (expression.kind === "record") {
|
|
130
|
+
return `{${expression.fields.map((field) => `"${field.name}": ${emitExpression(field.value)}`).join(", ")}}`;
|
|
131
|
+
}
|
|
132
|
+
if (expression.kind === "await") return `await ${emitExpression(expression.value)}`;
|
|
133
|
+
if (expression.kind === "property") return `${emitExpression(expression.target)}[${JSON.stringify(expression.name)}]`;
|
|
134
|
+
if (expression.kind === "call") {
|
|
135
|
+
if (expression.callee === "Error") {
|
|
136
|
+
const message = expression.args[0] ? emitExpression(expression.args[0]) : '""';
|
|
137
|
+
return `{"message": ${message}}`;
|
|
138
|
+
}
|
|
139
|
+
return `${expression.callee}(${expression.args.map(emitExpression).join(", ")})`;
|
|
140
|
+
}
|
|
141
|
+
const operator = BINARY_OPERATORS[expression.operator] ?? expression.operator;
|
|
142
|
+
return `(${emitExpression(expression.left)} ${operator} ${emitExpression(expression.right)})`;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function emitLiteral(value: unknown): string {
|
|
146
|
+
if (value === null) return "None";
|
|
147
|
+
if (value === true) return "True";
|
|
148
|
+
if (value === false) return "False";
|
|
149
|
+
if (typeof value === "string") return JSON.stringify(value);
|
|
150
|
+
return String(value);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function emitCondition(expression: PointCoreExpression): string {
|
|
154
|
+
const emitted = emitExpression(expression);
|
|
155
|
+
return emitted.startsWith("(") && emitted.endsWith(")") ? emitted.slice(1, -1) : emitted;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function toPythonModuleName(from: string): string {
|
|
159
|
+
return from.replace(/\.ts$/, "").replace(/\.js$/, "").replace(/\.py$/, "");
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function isPrimitiveType(type: string): type is PointCorePrimitiveType {
|
|
163
|
+
return type === "Text" || type === "Int" || type === "Float" || type === "Bool" || type === "Void";
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function indentLines(lines: string[]): string[] {
|
|
167
|
+
return lines.map((line) => ` ${line}`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function trimTrailingBlankLines(lines: string[]): string[] {
|
|
171
|
+
while (lines.at(-1) === "") lines.pop();
|
|
172
|
+
return lines;
|
|
173
|
+
}
|
|
@@ -68,8 +68,11 @@ function emitFunction(declaration: PointCoreFunctionDeclaration): string[] {
|
|
|
68
68
|
|
|
69
69
|
function emitStatement(statement: PointCoreStatement, semanticKind?: string): string[] {
|
|
70
70
|
if (statement.kind === "return") {
|
|
71
|
-
if (semanticKind === "view" && statement.value
|
|
72
|
-
|
|
71
|
+
if (semanticKind === "view" && statement.value) {
|
|
72
|
+
if (statement.value.kind === "literal" && typeof statement.value.value === "string") {
|
|
73
|
+
return [`return <>${escapeJsxText(statement.value.value)}</>;`];
|
|
74
|
+
}
|
|
75
|
+
return [`return <>{${emitExpression(statement.value)}}</>;`];
|
|
73
76
|
}
|
|
74
77
|
return [statement.value ? `return ${emitExpression(statement.value)};` : "return;"];
|
|
75
78
|
}
|
package/src/core/index.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
export * from "./ast.ts";
|
|
2
2
|
export * from "./check.ts";
|
|
3
3
|
export { findRunEntryName } from "./cli.ts";
|
|
4
|
+
export * from "./check-docs.ts";
|
|
4
5
|
export * from "./context.ts";
|
|
5
6
|
export * from "./emit-typescript.ts";
|
|
6
7
|
export * from "./emit-javascript.ts";
|
|
8
|
+
export * from "./emit-python.ts";
|
|
7
9
|
export * from "./incremental.ts";
|
|
8
10
|
export * from "./serialize.ts";
|
|
9
11
|
export * from "../semantic/index.ts";
|
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
|
}
|
|
@@ -138,9 +138,8 @@ function parsePrimaryExpression(source: string, context: PointSemanticExpression
|
|
|
138
138
|
return parseRecordExpression(trimmed, context);
|
|
139
139
|
}
|
|
140
140
|
if (trimmed.startsWith('"')) {
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
return { expression: { kind: "literal", value }, consumed: trimmed.slice(end + 1) };
|
|
141
|
+
const literal = parseJsonStringLiteral(trimmed);
|
|
142
|
+
return { expression: { kind: "literal", value: literal.value }, consumed: literal.consumed };
|
|
144
143
|
}
|
|
145
144
|
if (/^(true|false|null|none)\b/.test(trimmed)) {
|
|
146
145
|
const match = trimmed.match(/^(true|false|null|none)\b/);
|
|
@@ -183,6 +182,24 @@ function parsePrimaryExpression(source: string, context: PointSemanticExpression
|
|
|
183
182
|
throw new Error(`Unable to parse semantic expression: ${source}`);
|
|
184
183
|
}
|
|
185
184
|
|
|
185
|
+
function parseJsonStringLiteral(source: string): { value: string; consumed: string } {
|
|
186
|
+
if (!source.startsWith('"')) throw new Error(`Expected string literal: ${source}`);
|
|
187
|
+
let index = 1;
|
|
188
|
+
while (index < source.length) {
|
|
189
|
+
const char = source[index];
|
|
190
|
+
if (char === "\\") {
|
|
191
|
+
index += 2;
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
if (char === '"') {
|
|
195
|
+
const value = JSON.parse(source.slice(0, index + 1)) as string;
|
|
196
|
+
return { value, consumed: source.slice(index + 1) };
|
|
197
|
+
}
|
|
198
|
+
index += 1;
|
|
199
|
+
}
|
|
200
|
+
throw new Error(`Unterminated string literal: ${source}`);
|
|
201
|
+
}
|
|
202
|
+
|
|
186
203
|
function parseListExpression(source: string, context: PointSemanticExpressionContext): { expression: PointSemanticExpression; consumed: string } {
|
|
187
204
|
let rest = source.trim().slice(1).trimStart();
|
|
188
205
|
const items: PointSemanticExpression[] = [];
|