@fazelstudio/floe 1.0.0
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/LICENSE +21 -0
- package/dist/src/cli/check.d.ts +14 -0
- package/dist/src/cli/check.d.ts.map +1 -0
- package/dist/src/cli/check.js +88 -0
- package/dist/src/cli/check.js.map +1 -0
- package/dist/src/cli/format.d.ts +9 -0
- package/dist/src/cli/format.d.ts.map +1 -0
- package/dist/src/cli/format.js +114 -0
- package/dist/src/cli/format.js.map +1 -0
- package/dist/src/cli/main.d.ts +19 -0
- package/dist/src/cli/main.d.ts.map +1 -0
- package/dist/src/cli/main.js +207 -0
- package/dist/src/cli/main.js.map +1 -0
- package/dist/src/cli/render.d.ts +7 -0
- package/dist/src/cli/render.d.ts.map +1 -0
- package/dist/src/cli/render.js +90 -0
- package/dist/src/cli/render.js.map +1 -0
- package/dist/src/diagnostics.d.ts +32 -0
- package/dist/src/diagnostics.d.ts.map +1 -0
- package/dist/src/diagnostics.js +4 -0
- package/dist/src/diagnostics.js.map +1 -0
- package/dist/src/index.d.ts +40 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +64 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/language/completion.d.ts +6 -0
- package/dist/src/language/completion.d.ts.map +1 -0
- package/dist/src/language/completion.js +363 -0
- package/dist/src/language/completion.js.map +1 -0
- package/dist/src/language/definitions.d.ts +9 -0
- package/dist/src/language/definitions.d.ts.map +1 -0
- package/dist/src/language/definitions.js +70 -0
- package/dist/src/language/definitions.js.map +1 -0
- package/dist/src/language/diagnosticsService.d.ts +14 -0
- package/dist/src/language/diagnosticsService.d.ts.map +1 -0
- package/dist/src/language/diagnosticsService.js +37 -0
- package/dist/src/language/diagnosticsService.js.map +1 -0
- package/dist/src/language/folding.d.ts +8 -0
- package/dist/src/language/folding.d.ts.map +1 -0
- package/dist/src/language/folding.js +58 -0
- package/dist/src/language/folding.js.map +1 -0
- package/dist/src/language/formatting.d.ts +5 -0
- package/dist/src/language/formatting.d.ts.map +1 -0
- package/dist/src/language/formatting.js +274 -0
- package/dist/src/language/formatting.js.map +1 -0
- package/dist/src/language/highlighting.d.ts +9 -0
- package/dist/src/language/highlighting.d.ts.map +1 -0
- package/dist/src/language/highlighting.js +74 -0
- package/dist/src/language/highlighting.js.map +1 -0
- package/dist/src/language/hover.d.ts +6 -0
- package/dist/src/language/hover.d.ts.map +1 -0
- package/dist/src/language/hover.js +197 -0
- package/dist/src/language/hover.js.map +1 -0
- package/dist/src/language/indentation.d.ts +15 -0
- package/dist/src/language/indentation.d.ts.map +1 -0
- package/dist/src/language/indentation.js +97 -0
- package/dist/src/language/indentation.js.map +1 -0
- package/dist/src/language/index.d.ts +54 -0
- package/dist/src/language/index.d.ts.map +1 -0
- package/dist/src/language/index.js +85 -0
- package/dist/src/language/index.js.map +1 -0
- package/dist/src/language/references.d.ts +12 -0
- package/dist/src/language/references.d.ts.map +1 -0
- package/dist/src/language/references.js +35 -0
- package/dist/src/language/references.js.map +1 -0
- package/dist/src/language/rename.d.ts +18 -0
- package/dist/src/language/rename.d.ts.map +1 -0
- package/dist/src/language/rename.js +53 -0
- package/dist/src/language/rename.js.map +1 -0
- package/dist/src/language/semanticModel.d.ts +21 -0
- package/dist/src/language/semanticModel.d.ts.map +1 -0
- package/dist/src/language/semanticModel.js +43 -0
- package/dist/src/language/semanticModel.js.map +1 -0
- package/dist/src/language/symbols.d.ts +9 -0
- package/dist/src/language/symbols.d.ts.map +1 -0
- package/dist/src/language/symbols.js +160 -0
- package/dist/src/language/symbols.js.map +1 -0
- package/dist/src/language/types.d.ts +49 -0
- package/dist/src/language/types.d.ts.map +1 -0
- package/dist/src/language/types.js +2 -0
- package/dist/src/language/types.js.map +1 -0
- package/dist/src/language/utils.d.ts +24 -0
- package/dist/src/language/utils.d.ts.map +1 -0
- package/dist/src/language/utils.js +154 -0
- package/dist/src/language/utils.js.map +1 -0
- package/dist/src/layout/dagre.d.ts +13 -0
- package/dist/src/layout/dagre.d.ts.map +1 -0
- package/dist/src/layout/dagre.js +229 -0
- package/dist/src/layout/dagre.js.map +1 -0
- package/dist/src/layout/index.d.ts +15 -0
- package/dist/src/layout/index.d.ts.map +1 -0
- package/dist/src/layout/index.js +22 -0
- package/dist/src/layout/index.js.map +1 -0
- package/dist/src/layout/simple.d.ts +20 -0
- package/dist/src/layout/simple.d.ts.map +1 -0
- package/dist/src/layout/simple.js +674 -0
- package/dist/src/layout/simple.js.map +1 -0
- package/dist/src/layout/types.d.ts +65 -0
- package/dist/src/layout/types.d.ts.map +1 -0
- package/dist/src/layout/types.js +2 -0
- package/dist/src/layout/types.js.map +1 -0
- package/dist/src/lexer.d.ts +25 -0
- package/dist/src/lexer.d.ts.map +1 -0
- package/dist/src/lexer.js +280 -0
- package/dist/src/lexer.js.map +1 -0
- package/dist/src/lsp/documents.d.ts +24 -0
- package/dist/src/lsp/documents.d.ts.map +1 -0
- package/dist/src/lsp/documents.js +97 -0
- package/dist/src/lsp/documents.js.map +1 -0
- package/dist/src/lsp/server.d.ts +70 -0
- package/dist/src/lsp/server.d.ts.map +1 -0
- package/dist/src/lsp/server.js +623 -0
- package/dist/src/lsp/server.js.map +1 -0
- package/dist/src/lsp/utils.d.ts +47 -0
- package/dist/src/lsp/utils.d.ts.map +1 -0
- package/dist/src/lsp/utils.js +142 -0
- package/dist/src/lsp/utils.js.map +1 -0
- package/dist/src/parser.d.ts +106 -0
- package/dist/src/parser.d.ts.map +1 -0
- package/dist/src/parser.js +1018 -0
- package/dist/src/parser.js.map +1 -0
- package/dist/src/pipeline.d.ts +51 -0
- package/dist/src/pipeline.d.ts.map +1 -0
- package/dist/src/pipeline.js +65 -0
- package/dist/src/pipeline.js.map +1 -0
- package/dist/src/range.d.ts +24 -0
- package/dist/src/range.d.ts.map +1 -0
- package/dist/src/range.js +20 -0
- package/dist/src/range.js.map +1 -0
- package/dist/src/render/index.d.ts +3 -0
- package/dist/src/render/index.d.ts.map +1 -0
- package/dist/src/render/index.js +3 -0
- package/dist/src/render/index.js.map +1 -0
- package/dist/src/render/shapes.d.ts +20 -0
- package/dist/src/render/shapes.d.ts.map +1 -0
- package/dist/src/render/shapes.js +110 -0
- package/dist/src/render/shapes.js.map +1 -0
- package/dist/src/render/svg.d.ts +19 -0
- package/dist/src/render/svg.d.ts.map +1 -0
- package/dist/src/render/svg.js +227 -0
- package/dist/src/render/svg.js.map +1 -0
- package/dist/src/security/url.d.ts +7 -0
- package/dist/src/security/url.d.ts.map +1 -0
- package/dist/src/security/url.js +27 -0
- package/dist/src/security/url.js.map +1 -0
- package/dist/src/types.d.ts +111 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +16 -0
- package/dist/src/types.js.map +1 -0
- package/dist/src/validator.d.ts +47 -0
- package/dist/src/validator.d.ts.map +1 -0
- package/dist/src/validator.js +235 -0
- package/dist/src/validator.js.map +1 -0
- package/package.json +90 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Floe LSP Server — editor-independent Language Server
|
|
3
|
+
*
|
|
4
|
+
* Architecture:
|
|
5
|
+
* Editor
|
|
6
|
+
* ↓ (LSP JSON-RPC over stdio)
|
|
7
|
+
* Floe LSP Server (this file)
|
|
8
|
+
* ↓
|
|
9
|
+
* Floe Language Services (src/language/*)
|
|
10
|
+
* ↓
|
|
11
|
+
* Parser / Semantic Model
|
|
12
|
+
*
|
|
13
|
+
* Capabilities:
|
|
14
|
+
* - diagnostics (publishDiagnostics)
|
|
15
|
+
* - completion
|
|
16
|
+
* - hover
|
|
17
|
+
* - go-to-definition
|
|
18
|
+
* - references
|
|
19
|
+
* - rename
|
|
20
|
+
* - formatting
|
|
21
|
+
* - symbols (documentSymbol)
|
|
22
|
+
* - folding (foldingRange)
|
|
23
|
+
*
|
|
24
|
+
* Reuses existing language services rather than duplicate parser logic.
|
|
25
|
+
* No editor-specific semantic implementation.
|
|
26
|
+
* No eval / new Function — treat .floe as untrusted.
|
|
27
|
+
*/
|
|
28
|
+
export interface LspServerOptions {
|
|
29
|
+
stdio?: boolean;
|
|
30
|
+
input?: NodeJS.ReadableStream;
|
|
31
|
+
output?: NodeJS.WritableStream;
|
|
32
|
+
}
|
|
33
|
+
export declare class FloeLspServer {
|
|
34
|
+
private docs;
|
|
35
|
+
private input;
|
|
36
|
+
private output;
|
|
37
|
+
private buffer;
|
|
38
|
+
private nextId;
|
|
39
|
+
private initialized;
|
|
40
|
+
private shutdownRequested;
|
|
41
|
+
constructor(opts?: LspServerOptions);
|
|
42
|
+
/**
|
|
43
|
+
* Start server listening on stdio
|
|
44
|
+
*/
|
|
45
|
+
start(): Promise<void>;
|
|
46
|
+
private processBuffer;
|
|
47
|
+
private send;
|
|
48
|
+
private sendResponse;
|
|
49
|
+
private sendError;
|
|
50
|
+
private sendNotification;
|
|
51
|
+
private handleMessage;
|
|
52
|
+
private handleRequest;
|
|
53
|
+
private handleNotification;
|
|
54
|
+
private handleInitialize;
|
|
55
|
+
private handleDidOpen;
|
|
56
|
+
private handleDidChange;
|
|
57
|
+
private handleDidClose;
|
|
58
|
+
private publishDiagnostics;
|
|
59
|
+
private handleCompletion;
|
|
60
|
+
private handleHover;
|
|
61
|
+
private handleDefinition;
|
|
62
|
+
private handleReferences;
|
|
63
|
+
private handlePrepareRename;
|
|
64
|
+
private handleRename;
|
|
65
|
+
private handleFormatting;
|
|
66
|
+
private handleDocumentSymbol;
|
|
67
|
+
private handleFoldingRange;
|
|
68
|
+
}
|
|
69
|
+
export declare function startLspServer(opts?: LspServerOptions): Promise<FloeLspServer>;
|
|
70
|
+
//# sourceMappingURL=server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../../src/lsp/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AA6BH,MAAM,WAAW,gBAAgB;IAC/B,KAAK,CAAC,EAAE,OAAO,CAAC;IAEhB,KAAK,CAAC,EAAE,MAAM,CAAC,cAAc,CAAC;IAC9B,MAAM,CAAC,EAAE,MAAM,CAAC,cAAc,CAAC;CAChC;AAED,qBAAa,aAAa;IACxB,OAAO,CAAC,IAAI,CAAyB;IACrC,OAAO,CAAC,KAAK,CAAwB;IACrC,OAAO,CAAC,MAAM,CAAwB;IACtC,OAAO,CAAC,MAAM,CAAM;IACpB,OAAO,CAAC,MAAM,CAAK;IACnB,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,iBAAiB,CAAS;gBAEtB,IAAI,GAAE,gBAAqB;IAKvC;;OAEG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAyB5B,OAAO,CAAC,aAAa;IA6BrB,OAAO,CAAC,IAAI;IAMZ,OAAO,CAAC,YAAY;IAIpB,OAAO,CAAC,SAAS;IAIjB,OAAO,CAAC,gBAAgB;YAIV,aAAa;YAeb,aAAa;YA0Eb,kBAAkB;IA2ChC,OAAO,CAAC,gBAAgB;IAkCxB,OAAO,CAAC,aAAa;IAWrB,OAAO,CAAC,eAAe;IAqBvB,OAAO,CAAC,cAAc;IAQtB,OAAO,CAAC,kBAAkB;IA4B1B,OAAO,CAAC,gBAAgB;IA0BxB,OAAO,CAAC,WAAW;IAoBnB,OAAO,CAAC,gBAAgB;IAqBxB,OAAO,CAAC,gBAAgB;IAmBxB,OAAO,CAAC,mBAAmB;IA0B3B,OAAO,CAAC,YAAY;IA+BpB,OAAO,CAAC,gBAAgB;IAsBxB,OAAO,CAAC,oBAAoB;IA8B5B,OAAO,CAAC,kBAAkB;CAmB3B;AA0BD,wBAAsB,cAAc,CAAC,IAAI,GAAE,gBAAqB,GAAG,OAAO,CAAC,aAAa,CAAC,CAIxF"}
|
|
@@ -0,0 +1,623 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Floe LSP Server — editor-independent Language Server
|
|
3
|
+
*
|
|
4
|
+
* Architecture:
|
|
5
|
+
* Editor
|
|
6
|
+
* ↓ (LSP JSON-RPC over stdio)
|
|
7
|
+
* Floe LSP Server (this file)
|
|
8
|
+
* ↓
|
|
9
|
+
* Floe Language Services (src/language/*)
|
|
10
|
+
* ↓
|
|
11
|
+
* Parser / Semantic Model
|
|
12
|
+
*
|
|
13
|
+
* Capabilities:
|
|
14
|
+
* - diagnostics (publishDiagnostics)
|
|
15
|
+
* - completion
|
|
16
|
+
* - hover
|
|
17
|
+
* - go-to-definition
|
|
18
|
+
* - references
|
|
19
|
+
* - rename
|
|
20
|
+
* - formatting
|
|
21
|
+
* - symbols (documentSymbol)
|
|
22
|
+
* - folding (foldingRange)
|
|
23
|
+
*
|
|
24
|
+
* Reuses existing language services rather than duplicate parser logic.
|
|
25
|
+
* No editor-specific semantic implementation.
|
|
26
|
+
* No eval / new Function — treat .floe as untrusted.
|
|
27
|
+
*/
|
|
28
|
+
import * as readline from "node:readline";
|
|
29
|
+
import { DocumentManager } from "./documents.js";
|
|
30
|
+
import { offsetToLspPosition, lspPositionToOffset, floeRangeToLspRange, severityToLsp } from "./utils.js";
|
|
31
|
+
// Reuse language services — these are editor-independent
|
|
32
|
+
import { getDiagnostics } from "../language/diagnosticsService.js";
|
|
33
|
+
import { getCompletions } from "../language/completion.js";
|
|
34
|
+
import { getHover } from "../language/hover.js";
|
|
35
|
+
import { getDefinition } from "../language/definitions.js";
|
|
36
|
+
import { getReferences } from "../language/references.js";
|
|
37
|
+
import { rename } from "../language/rename.js";
|
|
38
|
+
import { format } from "../language/formatting.js";
|
|
39
|
+
import { getSymbols } from "../language/symbols.js";
|
|
40
|
+
import { getFoldingRanges } from "../language/folding.js";
|
|
41
|
+
export class FloeLspServer {
|
|
42
|
+
docs = new DocumentManager();
|
|
43
|
+
input;
|
|
44
|
+
output;
|
|
45
|
+
buffer = "";
|
|
46
|
+
nextId = 1;
|
|
47
|
+
initialized = false;
|
|
48
|
+
shutdownRequested = false;
|
|
49
|
+
constructor(opts = {}) {
|
|
50
|
+
this.input = opts.input ?? process.stdin;
|
|
51
|
+
this.output = opts.output ?? process.stdout;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Start server listening on stdio
|
|
55
|
+
*/
|
|
56
|
+
async start() {
|
|
57
|
+
this.input.on("data", (chunk) => {
|
|
58
|
+
this.buffer += chunk.toString("utf-8");
|
|
59
|
+
this.processBuffer();
|
|
60
|
+
});
|
|
61
|
+
this.input.on("end", () => {
|
|
62
|
+
if (!this.shutdownRequested) {
|
|
63
|
+
// Client closed
|
|
64
|
+
process.exit(0);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
// Handle errors
|
|
68
|
+
this.input.on("error", (err) => {
|
|
69
|
+
console.error(`LSP input error: ${err.message}`);
|
|
70
|
+
});
|
|
71
|
+
// Also handle SIGPIPE? Ensure we stay alive
|
|
72
|
+
// For testing, return immediately; for prod, keep alive
|
|
73
|
+
// We don't block here; event loop will keep running via stdin listener
|
|
74
|
+
if (this.input.isTTY) {
|
|
75
|
+
// No-op for test environments where input is not a stream
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
processBuffer() {
|
|
79
|
+
while (true) {
|
|
80
|
+
// LSP header: Content-Length: <num>\r\n\r\n
|
|
81
|
+
const headerEnd = this.buffer.indexOf("\r\n\r\n");
|
|
82
|
+
if (headerEnd === -1)
|
|
83
|
+
break;
|
|
84
|
+
const header = this.buffer.slice(0, headerEnd);
|
|
85
|
+
const match = header.match(/Content-Length:\s*(\d+)/i);
|
|
86
|
+
if (!match) {
|
|
87
|
+
// Malformed header, discard
|
|
88
|
+
this.buffer = this.buffer.slice(headerEnd + 4);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
const length = parseInt(match[1], 10);
|
|
92
|
+
const totalLength = headerEnd + 4 + length;
|
|
93
|
+
if (this.buffer.length < totalLength)
|
|
94
|
+
break; // Wait for more data
|
|
95
|
+
const body = this.buffer.slice(headerEnd + 4, totalLength);
|
|
96
|
+
this.buffer = this.buffer.slice(totalLength);
|
|
97
|
+
try {
|
|
98
|
+
const msg = JSON.parse(body);
|
|
99
|
+
this.handleMessage(msg);
|
|
100
|
+
}
|
|
101
|
+
catch (e) {
|
|
102
|
+
this.sendError(null, -32700, `Parse error: ${e instanceof Error ? e.message : String(e)}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
send(msg) {
|
|
107
|
+
const body = JSON.stringify(msg);
|
|
108
|
+
const header = `Content-Length: ${Buffer.byteLength(body, "utf-8")}\r\n\r\n`;
|
|
109
|
+
this.output.write(header + body);
|
|
110
|
+
}
|
|
111
|
+
sendResponse(id, result) {
|
|
112
|
+
this.send({ jsonrpc: "2.0", id, result });
|
|
113
|
+
}
|
|
114
|
+
sendError(id, code, message, data) {
|
|
115
|
+
this.send({ jsonrpc: "2.0", id, error: { code, message, data } });
|
|
116
|
+
}
|
|
117
|
+
sendNotification(method, params) {
|
|
118
|
+
this.send({ jsonrpc: "2.0", method, params });
|
|
119
|
+
}
|
|
120
|
+
async handleMessage(msg) {
|
|
121
|
+
if (msg.method) {
|
|
122
|
+
// Request or notification
|
|
123
|
+
if (msg.id !== undefined) {
|
|
124
|
+
// Request - needs response
|
|
125
|
+
await this.handleRequest(msg);
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
// Notification
|
|
129
|
+
await this.handleNotification(msg);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
else if (msg.id !== undefined && (msg.result !== undefined || msg.error !== undefined)) {
|
|
133
|
+
// Response to our request (not used currently)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
async handleRequest(msg) {
|
|
137
|
+
const method = msg.method;
|
|
138
|
+
const id = msg.id ?? null;
|
|
139
|
+
const params = msg.params;
|
|
140
|
+
try {
|
|
141
|
+
switch (method) {
|
|
142
|
+
case "initialize": {
|
|
143
|
+
const result = this.handleInitialize(params);
|
|
144
|
+
this.sendResponse(id, result);
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
case "shutdown": {
|
|
148
|
+
this.shutdownRequested = true;
|
|
149
|
+
this.sendResponse(id, null);
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
case "textDocument/completion": {
|
|
153
|
+
const result = this.handleCompletion(params);
|
|
154
|
+
this.sendResponse(id, result);
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
case "textDocument/hover": {
|
|
158
|
+
const result = this.handleHover(params);
|
|
159
|
+
this.sendResponse(id, result);
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
case "textDocument/definition": {
|
|
163
|
+
const result = this.handleDefinition(params);
|
|
164
|
+
this.sendResponse(id, result);
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
case "textDocument/references": {
|
|
168
|
+
const result = this.handleReferences(params);
|
|
169
|
+
this.sendResponse(id, result);
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
case "textDocument/rename": {
|
|
173
|
+
const result = this.handleRename(params);
|
|
174
|
+
this.sendResponse(id, result);
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
case "textDocument/formatting": {
|
|
178
|
+
const result = this.handleFormatting(params);
|
|
179
|
+
this.sendResponse(id, result);
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
182
|
+
case "textDocument/documentSymbol": {
|
|
183
|
+
const result = this.handleDocumentSymbol(params);
|
|
184
|
+
this.sendResponse(id, result);
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
case "textDocument/prepareRename": {
|
|
188
|
+
const result = this.handlePrepareRename(params);
|
|
189
|
+
this.sendResponse(id, result);
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
192
|
+
case "textDocument/foldingRange": {
|
|
193
|
+
const result = this.handleFoldingRange(params);
|
|
194
|
+
this.sendResponse(id, result);
|
|
195
|
+
break;
|
|
196
|
+
}
|
|
197
|
+
default: {
|
|
198
|
+
// Method not found
|
|
199
|
+
this.sendError(id, -32601, `Method not found: ${method}`);
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
catch (e) {
|
|
205
|
+
const msgText = e instanceof Error ? e.message : String(e);
|
|
206
|
+
this.sendError(id, -32603, `Internal error: ${msgText}`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
async handleNotification(msg) {
|
|
210
|
+
const method = msg.method;
|
|
211
|
+
const params = msg.params;
|
|
212
|
+
switch (method) {
|
|
213
|
+
case "initialized": {
|
|
214
|
+
this.initialized = true;
|
|
215
|
+
break;
|
|
216
|
+
}
|
|
217
|
+
case "exit": {
|
|
218
|
+
process.exit(this.shutdownRequested ? 0 : 1);
|
|
219
|
+
break;
|
|
220
|
+
}
|
|
221
|
+
case "textDocument/didOpen": {
|
|
222
|
+
this.handleDidOpen(params);
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
case "textDocument/didChange": {
|
|
226
|
+
this.handleDidChange(params);
|
|
227
|
+
break;
|
|
228
|
+
}
|
|
229
|
+
case "textDocument/didClose": {
|
|
230
|
+
this.handleDidClose(params);
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
case "textDocument/didSave": {
|
|
234
|
+
// Re-validate on save
|
|
235
|
+
if (params?.textDocument?.uri) {
|
|
236
|
+
this.publishDiagnostics(params.textDocument.uri);
|
|
237
|
+
}
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
case "$/cancelRequest": {
|
|
241
|
+
// Ignore
|
|
242
|
+
break;
|
|
243
|
+
}
|
|
244
|
+
default: {
|
|
245
|
+
// Unknown notification - ignore
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
handleInitialize(params) {
|
|
251
|
+
// Client capabilities ignored for now; we advertise our capabilities
|
|
252
|
+
return {
|
|
253
|
+
capabilities: {
|
|
254
|
+
textDocumentSync: {
|
|
255
|
+
openClose: true,
|
|
256
|
+
change: 1, // Full
|
|
257
|
+
save: { includeText: false },
|
|
258
|
+
},
|
|
259
|
+
completionProvider: {
|
|
260
|
+
triggerCharacters: [" ", "[", "-", ">", ":"],
|
|
261
|
+
resolveProvider: false,
|
|
262
|
+
},
|
|
263
|
+
hoverProvider: true,
|
|
264
|
+
definitionProvider: true,
|
|
265
|
+
referencesProvider: true,
|
|
266
|
+
renameProvider: {
|
|
267
|
+
prepareProvider: true,
|
|
268
|
+
},
|
|
269
|
+
documentFormattingProvider: true,
|
|
270
|
+
documentSymbolProvider: true,
|
|
271
|
+
foldingRangeProvider: true,
|
|
272
|
+
diagnosticProvider: {
|
|
273
|
+
interFileDependencies: false,
|
|
274
|
+
workspaceDiagnostics: false,
|
|
275
|
+
},
|
|
276
|
+
},
|
|
277
|
+
serverInfo: {
|
|
278
|
+
name: "floe-lsp",
|
|
279
|
+
version: "1.0.0",
|
|
280
|
+
},
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
handleDidOpen(params) {
|
|
284
|
+
const doc = params?.textDocument;
|
|
285
|
+
if (!doc?.uri || typeof doc.text !== "string")
|
|
286
|
+
return;
|
|
287
|
+
const uri = doc.uri;
|
|
288
|
+
const text = doc.text;
|
|
289
|
+
const version = doc.version ?? 1;
|
|
290
|
+
const languageId = doc.languageId ?? "floe";
|
|
291
|
+
this.docs.open(uri, languageId, version, text);
|
|
292
|
+
this.publishDiagnostics(uri);
|
|
293
|
+
}
|
|
294
|
+
handleDidChange(params) {
|
|
295
|
+
const doc = params?.textDocument;
|
|
296
|
+
const changes = params?.contentChanges;
|
|
297
|
+
if (!doc?.uri || !Array.isArray(changes))
|
|
298
|
+
return;
|
|
299
|
+
const uri = doc.uri;
|
|
300
|
+
const version = doc.version ?? 1;
|
|
301
|
+
// LSP spec: if changes have range, it's incremental; if no range, full
|
|
302
|
+
const hasRanges = changes.some((c) => c.range !== undefined);
|
|
303
|
+
if (hasRanges) {
|
|
304
|
+
// Incremental changes: need to apply ranges sequentially based on evolving text
|
|
305
|
+
// For Full sync (change:1), changes will be single entry with full text and no range, but handle both
|
|
306
|
+
this.docs.applyIncremental(uri, version, changes);
|
|
307
|
+
}
|
|
308
|
+
else {
|
|
309
|
+
// Full text sync
|
|
310
|
+
const text = changes[0]?.text ?? "";
|
|
311
|
+
this.docs.update(uri, version, text);
|
|
312
|
+
}
|
|
313
|
+
this.publishDiagnostics(uri);
|
|
314
|
+
}
|
|
315
|
+
handleDidClose(params) {
|
|
316
|
+
const uri = params?.textDocument?.uri;
|
|
317
|
+
if (!uri)
|
|
318
|
+
return;
|
|
319
|
+
this.docs.close(uri);
|
|
320
|
+
// Clear diagnostics for closed file
|
|
321
|
+
this.sendNotification("textDocument/publishDiagnostics", { uri, diagnostics: [] });
|
|
322
|
+
}
|
|
323
|
+
publishDiagnostics(uri) {
|
|
324
|
+
const doc = this.docs.get(uri);
|
|
325
|
+
if (!doc)
|
|
326
|
+
return;
|
|
327
|
+
const text = doc.text;
|
|
328
|
+
// Reuse language service diagnostics (editor-independent)
|
|
329
|
+
let diagnostics = [];
|
|
330
|
+
try {
|
|
331
|
+
const floeDiags = getDiagnostics(text);
|
|
332
|
+
diagnostics = floeDiags.map((d) => {
|
|
333
|
+
const lspRange = floeRangeToLspRange(text, d.range);
|
|
334
|
+
return {
|
|
335
|
+
range: lspRange,
|
|
336
|
+
severity: severityToLsp(d.severity),
|
|
337
|
+
code: d.code,
|
|
338
|
+
source: "floe",
|
|
339
|
+
message: d.message,
|
|
340
|
+
};
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
catch {
|
|
344
|
+
// Never crash on malformed input
|
|
345
|
+
diagnostics = [];
|
|
346
|
+
}
|
|
347
|
+
this.sendNotification("textDocument/publishDiagnostics", {
|
|
348
|
+
uri,
|
|
349
|
+
diagnostics,
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
handleCompletion(params) {
|
|
353
|
+
const uri = params?.textDocument?.uri;
|
|
354
|
+
const pos = params?.position;
|
|
355
|
+
if (!uri || !pos)
|
|
356
|
+
return null;
|
|
357
|
+
const doc = this.docs.get(uri);
|
|
358
|
+
if (!doc)
|
|
359
|
+
return null;
|
|
360
|
+
const offset = lspPositionToOffset(doc.text, pos);
|
|
361
|
+
try {
|
|
362
|
+
const items = getCompletions(doc.text, offset);
|
|
363
|
+
// Map to LSP CompletionItem
|
|
364
|
+
const lspItems = items.map((it) => ({
|
|
365
|
+
label: it.label,
|
|
366
|
+
kind: completionKindToLsp(it.kind),
|
|
367
|
+
detail: it.detail,
|
|
368
|
+
insertText: it.insertText ?? it.label,
|
|
369
|
+
sortText: it.sortText,
|
|
370
|
+
}));
|
|
371
|
+
return {
|
|
372
|
+
isIncomplete: false,
|
|
373
|
+
items: lspItems,
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
catch {
|
|
377
|
+
return { isIncomplete: false, items: [] };
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
handleHover(params) {
|
|
381
|
+
const uri = params?.textDocument?.uri;
|
|
382
|
+
const pos = params?.position;
|
|
383
|
+
if (!uri || !pos)
|
|
384
|
+
return null;
|
|
385
|
+
const doc = this.docs.get(uri);
|
|
386
|
+
if (!doc)
|
|
387
|
+
return null;
|
|
388
|
+
const offset = lspPositionToOffset(doc.text, pos);
|
|
389
|
+
try {
|
|
390
|
+
const hover = getHover(doc.text, offset);
|
|
391
|
+
if (!hover)
|
|
392
|
+
return null;
|
|
393
|
+
const lspRange = hover.range ? floeRangeToLspRange(doc.text, hover.range) : undefined;
|
|
394
|
+
return {
|
|
395
|
+
contents: hover.contents.map((c) => ({ language: "markdown", value: c })),
|
|
396
|
+
range: lspRange,
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
catch {
|
|
400
|
+
return null;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
handleDefinition(params) {
|
|
404
|
+
const uri = params?.textDocument?.uri;
|
|
405
|
+
const pos = params?.position;
|
|
406
|
+
if (!uri || !pos)
|
|
407
|
+
return null;
|
|
408
|
+
const doc = this.docs.get(uri);
|
|
409
|
+
if (!doc)
|
|
410
|
+
return null;
|
|
411
|
+
const offset = lspPositionToOffset(doc.text, pos);
|
|
412
|
+
try {
|
|
413
|
+
const def = getDefinition(doc.text, offset);
|
|
414
|
+
if (!def)
|
|
415
|
+
return null;
|
|
416
|
+
const lspRange = floeRangeToLspRange(doc.text, def.range);
|
|
417
|
+
return {
|
|
418
|
+
uri,
|
|
419
|
+
range: lspRange,
|
|
420
|
+
};
|
|
421
|
+
// Alternatively return Location[] array
|
|
422
|
+
}
|
|
423
|
+
catch {
|
|
424
|
+
return null;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
handleReferences(params) {
|
|
428
|
+
const uri = params?.textDocument?.uri;
|
|
429
|
+
const pos = params?.position;
|
|
430
|
+
if (!uri || !pos)
|
|
431
|
+
return [];
|
|
432
|
+
const doc = this.docs.get(uri);
|
|
433
|
+
if (!doc)
|
|
434
|
+
return [];
|
|
435
|
+
const offset = lspPositionToOffset(doc.text, pos);
|
|
436
|
+
try {
|
|
437
|
+
const refs = getReferences(doc.text, offset);
|
|
438
|
+
// getReferences returns Location with range only; need to attach uri
|
|
439
|
+
return refs.map((r) => ({
|
|
440
|
+
uri,
|
|
441
|
+
range: floeRangeToLspRange(doc.text, r.range),
|
|
442
|
+
}));
|
|
443
|
+
}
|
|
444
|
+
catch {
|
|
445
|
+
return [];
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
handlePrepareRename(params) {
|
|
449
|
+
const uri = params?.textDocument?.uri;
|
|
450
|
+
const pos = params?.position;
|
|
451
|
+
if (!uri || !pos)
|
|
452
|
+
return null;
|
|
453
|
+
const doc = this.docs.get(uri);
|
|
454
|
+
if (!doc)
|
|
455
|
+
return null;
|
|
456
|
+
const offset = lspPositionToOffset(doc.text, pos);
|
|
457
|
+
try {
|
|
458
|
+
// Use hover or definition to determine if at identifier
|
|
459
|
+
// We can try getReferences; if empty, not renameable
|
|
460
|
+
const refs = getReferences(doc.text, offset);
|
|
461
|
+
if (refs.length === 0)
|
|
462
|
+
return null;
|
|
463
|
+
// Also check that offset is on identifier via tokenization? For simplicity return range of first ref
|
|
464
|
+
// Find token range at offset
|
|
465
|
+
// Reuse getDefinition's logic: if definition exists, we have range
|
|
466
|
+
const def = getDefinition(doc.text, offset);
|
|
467
|
+
if (!def)
|
|
468
|
+
return null;
|
|
469
|
+
return {
|
|
470
|
+
range: floeRangeToLspRange(doc.text, def.range),
|
|
471
|
+
placeholder: doc.text.slice(def.range.start.offset, def.range.end.offset),
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
catch {
|
|
475
|
+
return null;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
handleRename(params) {
|
|
479
|
+
const uri = params?.textDocument?.uri;
|
|
480
|
+
const pos = params?.position;
|
|
481
|
+
const newName = params?.newName;
|
|
482
|
+
if (!uri || !pos || typeof newName !== "string")
|
|
483
|
+
return null;
|
|
484
|
+
const doc = this.docs.get(uri);
|
|
485
|
+
if (!doc)
|
|
486
|
+
return null;
|
|
487
|
+
const offset = lspPositionToOffset(doc.text, pos);
|
|
488
|
+
try {
|
|
489
|
+
const result = rename(doc.text, offset, newName);
|
|
490
|
+
if (result.error) {
|
|
491
|
+
// LSP rename error should be shown as response error or null?
|
|
492
|
+
// Return error viaJson RPC error? Instead return null and let client show? We'll send error
|
|
493
|
+
// But spec says rename returns WorkspaceEdit or null; invalid newName should be handled as error
|
|
494
|
+
// We'll return null and also send window/showMessage? For now return error as null and client will handle.
|
|
495
|
+
// Better to send error response via throw? But handler already catches.
|
|
496
|
+
// So we return null and log
|
|
497
|
+
return null;
|
|
498
|
+
}
|
|
499
|
+
// result.edits is array of TextEdit with Floe Range
|
|
500
|
+
const changes = {};
|
|
501
|
+
changes[uri] = result.edits.map((e) => ({
|
|
502
|
+
range: floeRangeToLspRange(doc.text, e.range),
|
|
503
|
+
newText: e.newText,
|
|
504
|
+
}));
|
|
505
|
+
return { changes };
|
|
506
|
+
}
|
|
507
|
+
catch {
|
|
508
|
+
return null;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
handleFormatting(params) {
|
|
512
|
+
const uri = params?.textDocument?.uri;
|
|
513
|
+
if (!uri)
|
|
514
|
+
return null;
|
|
515
|
+
const doc = this.docs.get(uri);
|
|
516
|
+
if (!doc)
|
|
517
|
+
return null;
|
|
518
|
+
try {
|
|
519
|
+
const formatted = format(doc.text);
|
|
520
|
+
if (formatted === doc.text)
|
|
521
|
+
return [];
|
|
522
|
+
// Single edit covering whole document
|
|
523
|
+
const start = { line: 0, character: 0 };
|
|
524
|
+
const end = offsetToLspPosition(doc.text, doc.text.length);
|
|
525
|
+
return [
|
|
526
|
+
{
|
|
527
|
+
range: { start, end },
|
|
528
|
+
newText: formatted,
|
|
529
|
+
},
|
|
530
|
+
];
|
|
531
|
+
}
|
|
532
|
+
catch {
|
|
533
|
+
return null;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
handleDocumentSymbol(params) {
|
|
537
|
+
const uri = params?.textDocument?.uri;
|
|
538
|
+
if (!uri)
|
|
539
|
+
return [];
|
|
540
|
+
const doc = this.docs.get(uri);
|
|
541
|
+
if (!doc)
|
|
542
|
+
return [];
|
|
543
|
+
try {
|
|
544
|
+
const symbols = getSymbols(doc.text);
|
|
545
|
+
// Convert to LSP DocumentSymbol (hierarchical)
|
|
546
|
+
const toLspSymbol = (s) => {
|
|
547
|
+
const lspRange = floeRangeToLspRange(doc.text, s.range);
|
|
548
|
+
const selectionRange = s.selectionRange ? floeRangeToLspRange(doc.text, s.selectionRange) : lspRange;
|
|
549
|
+
const kind = symbolKindToLsp(s.kind);
|
|
550
|
+
const result = {
|
|
551
|
+
name: s.name,
|
|
552
|
+
kind,
|
|
553
|
+
range: lspRange,
|
|
554
|
+
selectionRange,
|
|
555
|
+
detail: s.detail,
|
|
556
|
+
};
|
|
557
|
+
if (s.children && s.children.length > 0) {
|
|
558
|
+
result.children = s.children.map(toLspSymbol);
|
|
559
|
+
}
|
|
560
|
+
return result;
|
|
561
|
+
};
|
|
562
|
+
return symbols.map(toLspSymbol);
|
|
563
|
+
}
|
|
564
|
+
catch {
|
|
565
|
+
return [];
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
handleFoldingRange(params) {
|
|
569
|
+
const uri = params?.textDocument?.uri;
|
|
570
|
+
if (!uri)
|
|
571
|
+
return [];
|
|
572
|
+
const doc = this.docs.get(uri);
|
|
573
|
+
if (!doc)
|
|
574
|
+
return [];
|
|
575
|
+
try {
|
|
576
|
+
const ranges = getFoldingRanges(doc.text);
|
|
577
|
+
return ranges.map((r) => ({
|
|
578
|
+
startLine: r.startLine - 1,
|
|
579
|
+
endLine: r.endLine - 1,
|
|
580
|
+
startCharacter: r.startColumn ? r.startColumn - 1 : undefined,
|
|
581
|
+
endCharacter: r.endColumn ? r.endColumn - 1 : undefined,
|
|
582
|
+
kind: r.kind ?? "region",
|
|
583
|
+
collapsedText: r.collapsedText,
|
|
584
|
+
}));
|
|
585
|
+
}
|
|
586
|
+
catch {
|
|
587
|
+
return [];
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
function completionKindToLsp(kind) {
|
|
592
|
+
switch (kind) {
|
|
593
|
+
case "keyword": return 14; // Keyword
|
|
594
|
+
case "type": return 8; // Interface or Type? Use 8 Interface
|
|
595
|
+
case "variable": return 6; // Variable
|
|
596
|
+
case "value": return 12; // Value
|
|
597
|
+
case "constant": return 21; // Constant
|
|
598
|
+
default: return 1; // Text
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
function symbolKindToLsp(kind) {
|
|
602
|
+
switch (kind) {
|
|
603
|
+
case "node": return 5; // Class? Use 5 Class for node, or 13 Variable? Choose 5
|
|
604
|
+
case "group": return 4; // Class? Use 4 for group container? Actually 2 Module, 4 Class
|
|
605
|
+
case "edge": return 13; // Variable? Use 13 Variable for edge
|
|
606
|
+
case "direction": return 14; // Constant? Use 14 Constant
|
|
607
|
+
case "meta": return 13; // Variable
|
|
608
|
+
case "annotation": return 13;
|
|
609
|
+
case "link": return 13;
|
|
610
|
+
default: return 1; // File
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
export async function startLspServer(opts = {}) {
|
|
614
|
+
const server = new FloeLspServer(opts);
|
|
615
|
+
await server.start();
|
|
616
|
+
return server;
|
|
617
|
+
}
|
|
618
|
+
// If run directly via node
|
|
619
|
+
if (import.meta.url === `file://${process.argv[1]?.replace(/\\/g, "/")}`) {
|
|
620
|
+
const server = new FloeLspServer({ stdio: true });
|
|
621
|
+
server.start();
|
|
622
|
+
}
|
|
623
|
+
//# sourceMappingURL=server.js.map
|