forked from marimo-team/marimo
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathindex.ts
More file actions
220 lines (187 loc) · 5.88 KB
/
index.ts
File metadata and controls
220 lines (187 loc) · 5.88 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
import { appendFile, mkdir, writeFile } from "node:fs/promises";
import type { IncomingMessage } from "node:http";
import { dirname } from "node:path";
import parseArgs from "minimist";
import type { IWebSocket } from "vscode-ws-jsonrpc";
import {
createServerProcess,
createWebSocketConnection,
forward,
} from "vscode-ws-jsonrpc/server";
import type { CloseEvent, Data, ErrorEvent, MessageEvent, WebSocket } from "ws";
import { WebSocketServer } from "ws";
class Logger {
private constructor(private readonly logFilePath: string) {
this.logFilePath = logFilePath;
}
static async create(logFilePath: string): Promise<Logger> {
await mkdir(dirname(logFilePath), { recursive: true });
await writeFile(logFilePath, "");
return new Logger(logFilePath);
}
private async appendToLogFile(...args: unknown[]): Promise<void> {
const log = args.join(" ");
try {
await appendFile(this.logFilePath, `${log}\n`);
} catch (error) {
// biome-ignore lint/suspicious/noConsole: For printing to the console
console.error("Failed to write to log file:", error);
}
}
debug(...args: Parameters<typeof console.log>): void {
// biome-ignore lint/suspicious/noConsole: For printing to the console
console.log(...args);
void this.appendToLogFile("[DEBUG]", ...args);
}
log(...args: Parameters<typeof console.log>): void {
// biome-ignore lint/suspicious/noConsole: For printing to the console
console.log(...args);
void this.appendToLogFile("[INFO]", ...args);
}
error(...args: Parameters<typeof console.error>): void {
// biome-ignore lint/suspicious/noConsole: For printing to the console
console.error(...args);
void this.appendToLogFile("[ERROR]", ...args);
}
}
class WebSocketAdapter implements IWebSocket {
constructor(
private readonly webSocket: WebSocket,
private readonly logger: Logger,
) {
this.webSocket = webSocket;
this.logger = logger;
}
send(content: string): void {
try {
this.webSocket.send(content);
this.logger.debug("Sent message:", content);
} catch (error) {
this.logger.error("Failed to send message:", error);
}
}
onMessage(callback: (data: Data) => void): void {
this.webSocket.onmessage = (event: MessageEvent) => {
try {
callback(event.data);
this.logger.debug("Received message:", event.data);
} catch (error) {
this.logger.error("Error handling message:", error);
}
};
}
onError(callback: (message: string) => void): void {
this.webSocket.onerror = (event: ErrorEvent) => {
this.logger.error("WebSocket error:", event);
if ("message" in event) {
callback(event.message);
}
};
}
onClose(callback: (code: number, reason: string) => void): void {
this.webSocket.onclose = (event: CloseEvent) => {
this.logger.log(
`WebSocket closed with code ${event.code}: ${event.reason}`,
);
callback(event.code, event.reason);
};
}
dispose(): void {
this.webSocket.close();
}
}
export function parseTypedCommand(typedCommand: string): string[] {
if (!typedCommand.includes(":")) {
// Fallback for old format - simple split by spaces
return typedCommand.split(" ");
}
const colonIndex = typedCommand.indexOf(":");
const serverType = typedCommand.substring(0, colonIndex);
const binaryPath = typedCommand.substring(colonIndex + 1);
switch (serverType) {
case "copilot":
return ["node", binaryPath, "--stdio"];
case "basedpyright":
return [binaryPath, "--stdio"];
case "pyrefly":
return [binaryPath, "lsp"];
case "ty":
return [binaryPath, "server"];
default:
throw new Error(`Unknown LSP server type: ${serverType}`);
}
}
function handleWebSocketConnection(
languageServerCommand: string[],
logger: Logger,
webSocket: WebSocket,
_: IncomingMessage,
): void {
if (!languageServerCommand) {
webSocket.close();
return;
}
const jsonRpcConnection = createServerProcess(
languageServerCommand.join(" "),
languageServerCommand[0],
languageServerCommand.slice(1),
);
if (!jsonRpcConnection) {
throw new Error("Not able to create json-rpc connection.");
}
const socket = new WebSocketAdapter(webSocket, logger);
const connection = createWebSocketConnection(socket);
forward(connection, jsonRpcConnection);
socket.onClose(() => {
jsonRpcConnection.dispose();
connection.dispose();
});
connection.onClose(() => {
jsonRpcConnection.dispose();
socket.dispose();
});
}
function startWebSocketServer(
port: number,
languageServerCommand: string[],
logger: Logger,
): void {
const webSocketServer = new WebSocketServer({
port,
perMessageDeflate: false,
});
webSocketServer.on("error", (error) => {
logger.error("WebSocket server error:", error);
});
webSocketServer.on("connection", (webSocket, request) => {
logger.log(`New connection from ${request.socket.remoteAddress}`);
try {
handleWebSocketConnection(
languageServerCommand,
logger,
webSocket,
request,
);
} catch (error) {
logger.error("Failed to start WebSocket bridge:", error);
webSocket.close();
}
});
}
async function main(): Promise<void> {
const argv = parseArgs(process.argv);
if (argv.help) {
// biome-ignore lint/suspicious/noConsole: For printing to the console
console.log(
'Usage: node index.cjs --log-file <path> --lsp "<command>" [--port <port>]',
);
return;
}
const logFile = argv["log-file"] || "/tmp/lsp-server.log";
const logger = await Logger.create(logFile);
const serverPort = Number.parseInt(argv.port, 10) || 3000;
const languageServerCommand = parseTypedCommand(argv.lsp || "echo test");
logger.log(`Parsed LSP command: ${languageServerCommand.join(" ")}`);
startWebSocketServer(serverPort, languageServerCommand, logger);
}
void main();