|
1 | 1 | import fastify from 'fastify'; |
2 | | -import { readFileSync, readdirSync, mkdirSync, writeFileSync, rmSync, existsSync } from "fs"; |
3 | | -import { join } from "path"; |
| 2 | +import { readFileSync, readdirSync } from "fs"; |
4 | 3 | import fastifyCors from 'fastify-cors'; |
5 | 4 | import fastifyWebSocket from 'fastify-websocket'; |
6 | | -import * as ws from 'ws'; |
7 | | -import * as rpc from 'vscode-ws-jsonrpc'; |
8 | | -import * as rpcServer from 'vscode-ws-jsonrpc/lib/server'; |
9 | 5 | import { build_project as build_c_project, requestBodySchema as requestCBodySchema, RequestBody as RequestCBody } from './chooks'; |
10 | 6 | import { build_project as build_js_project, requestBodySchema as requestJSBodySchema, RequestBody as RequestJSBody } from './jshooks'; |
| 7 | +import { handleCLanguageServer } from './language-server/c'; |
11 | 8 |
|
12 | 9 | const server = fastify(); |
13 | 10 |
|
@@ -88,276 +85,10 @@ server.get('/', async (req, reply) => { |
88 | 85 | reply.code(200).send('ok') |
89 | 86 | }) |
90 | 87 |
|
91 | | -function toSocket(webSocket: ws): rpc.IWebSocket { |
92 | | - return { |
93 | | - send: content => webSocket.send(content), |
94 | | - onMessage: cb => webSocket.onmessage = event => cb(event.data), |
95 | | - onError: cb => webSocket.onerror = event => { |
96 | | - if ('message' in event) { |
97 | | - cb((event as any).message) |
98 | | - } |
99 | | - }, |
100 | | - onClose: cb => webSocket.onclose = event => cb(event.code, event.reason), |
101 | | - dispose: () => webSocket.close() |
102 | | - } |
103 | | -} |
104 | | - |
105 | 88 | server.get('/language-server/c', { websocket: true }, (connection /* SocketStream */, req /* FastifyRequest */) => { |
106 | | - // create a temporary directory for the workspace |
107 | | - const workspaceDir = tempDir + '/language-server' + '/ls_' + Math.random().toString(36).slice(2); |
108 | | - |
109 | | - console.log('Workspace directory: ', workspaceDir); |
110 | | - mkdirSync(workspaceDir); |
111 | | - console.log('mkdirSync: ') |
112 | | - |
113 | | - console.log(`Creating workspace directory: ${workspaceDir}`); |
114 | | - |
115 | | - // run clangd with the following arguments |
116 | | - let localConnection = rpcServer.createServerProcess('Clangd process', 'clangd', [ |
117 | | - `--compile-commands-dir=/etc/clangd`, |
118 | | - `--limit-results=200`, |
119 | | - `--background-index=false` |
120 | | - ], { |
121 | | - cwd: workspaceDir |
122 | | - }); |
123 | | - |
124 | | - // intercept messages from the client and process them |
125 | | - let initialized = false; |
126 | | - let messageHandler: ((data: any) => void) | null = null; |
127 | | - const openDocuments = new Set<string>(); // track opened documents |
128 | | - |
129 | | - // helper function to ensure a file is opened in clangd |
130 | | - const ensureDocumentOpen = (uri: string, text?: string): string => { |
131 | | - const fileName = uri.replace(/^file:\/\//, ''); |
132 | | - const filePath = join(workspaceDir, fileName); |
133 | | - const fileUri = `file://${filePath}`; |
134 | | - |
135 | | - if (!openDocuments.has(fileUri)) { |
136 | | - console.log('Auto-opening document:', fileUri); |
137 | | - |
138 | | - // create the directory if it doesn't exist |
139 | | - const dir = join(workspaceDir, fileName.split('/').slice(0, -1).join('/')); |
140 | | - if (dir !== workspaceDir) { |
141 | | - mkdirSync(dir, { recursive: true }); |
142 | | - } |
143 | | - |
144 | | - // read file content if not provided |
145 | | - let fileContent = text; |
146 | | - console.log('fileContent: ', fileContent); |
147 | | - if (!fileContent && existsSync(filePath)) { |
148 | | - fileContent = readFileSync(filePath, 'utf-8'); |
149 | | - } else if (!fileContent) { |
150 | | - fileContent = ''; // empty file if it doesn't exist |
151 | | - } |
152 | | - |
153 | | - // write the file to the temporary directory |
154 | | - writeFileSync(filePath, fileContent); |
155 | | - |
156 | | - // send textDocument/didOpen to clangd |
157 | | - if (messageHandler) { |
158 | | - const didOpenMessage = { |
159 | | - jsonrpc: '2.0', |
160 | | - method: 'textDocument/didOpen', |
161 | | - params: { |
162 | | - textDocument: { |
163 | | - uri: fileUri, |
164 | | - languageId: fileName.endsWith('.h') ? 'c' : 'c', |
165 | | - version: 1, |
166 | | - text: fileContent |
167 | | - } |
168 | | - } |
169 | | - }; |
170 | | - console.log('Sending didOpen for:', fileUri); |
171 | | - messageHandler(JSON.stringify(didOpenMessage)); |
172 | | - } |
173 | | - |
174 | | - openDocuments.add(fileUri); |
175 | | - } |
176 | | - |
177 | | - return fileUri; |
178 | | - }; |
179 | | - |
180 | | - // helper function to convert LSP Position to text offset |
181 | | - const positionToOffset = (text: string, position: { line: number; character: number }): number => { |
182 | | - const lines = text.split('\n'); |
183 | | - let offset = 0; |
184 | | - for (let i = 0; i < position.line && i < lines.length; i++) { |
185 | | - offset += lines[i].length + 1; // +1 for newline character |
186 | | - } |
187 | | - return offset + Math.min(position.character, lines[position.line]?.length || 0); |
188 | | - }; |
189 | | - |
190 | | - // helper function to apply a single change to text |
191 | | - const applyChange = (originalText: string, change: { |
192 | | - range?: { start: { line: number; character: number }; end: { line: number; character: number } }; |
193 | | - rangeLength?: number; |
194 | | - text: string; |
195 | | - }): string => { |
196 | | - // Full text replacement (no range specified) |
197 | | - if (!change.range) { |
198 | | - return change.text; |
199 | | - } |
200 | | - |
201 | | - // Range-based replacement |
202 | | - const startOffset = positionToOffset(originalText, change.range.start); |
203 | | - const endOffset = positionToOffset(originalText, change.range.end); |
204 | | - |
205 | | - return originalText.slice(0, startOffset) + change.text + originalText.slice(endOffset); |
206 | | - }; |
207 | | - |
208 | | - // create a custom IWebSocket that intercepts messages |
209 | | - const interceptedSocket: rpc.IWebSocket = { |
210 | | - send: (content) => connection.socket.send(content), |
211 | | - onMessage: (cb) => { |
212 | | - console.log('onMessage callback registered'); |
213 | | - messageHandler = cb; |
214 | | - connection.socket.onmessage = (event) => { |
215 | | - const data = event.data; |
216 | | - console.log('Raw message received:', data.toString().substring(0, 100)); |
217 | | - |
218 | | - try { |
219 | | - const message = JSON.parse(data.toString()); |
220 | | - console.log('Parsed message method:', message.method); |
221 | | - |
222 | | - // set the workspace folder |
223 | | - if (message.method === 'initialize' && !initialized) { |
224 | | - initialized = true; |
225 | | - console.log('Initializing workspace:', workspaceDir); |
226 | | - // set the workspace folder |
227 | | - if (!message.params) { |
228 | | - message.params = {}; |
229 | | - } |
230 | | - if (!message.params.workspaceFolders) { |
231 | | - message.params.workspaceFolders = [{ |
232 | | - uri: `file://${workspaceDir}`, |
233 | | - name: 'workspace' |
234 | | - }]; |
235 | | - } |
236 | | - } |
237 | | - |
238 | | - // handle textDocument/didOpen messages and write the file to the temporary directory |
239 | | - if (message.method === 'textDocument/didOpen' && message.params?.textDocument) { |
240 | | - const uri = message.params.textDocument.uri; |
241 | | - const text = message.params.textDocument.text; |
242 | | - |
243 | | - console.log('textDocument/didOpen received, URI:', uri); |
244 | | - |
245 | | - // extract the file name from the URI (example: file://file.c -> file.c) |
246 | | - const fileName = uri.replace(/^file:\/\//, ''); |
247 | | - const filePath = join(workspaceDir, fileName); |
248 | | - |
249 | | - // create the directory if it doesn't exist |
250 | | - const dir = join(workspaceDir, fileName.split('/').slice(0, -1).join('/')); |
251 | | - if (dir !== workspaceDir) { |
252 | | - mkdirSync(dir, { recursive: true }); |
253 | | - } |
254 | | - |
255 | | - // write the file to the temporary directory |
256 | | - console.log('Writing file to ', filePath); |
257 | | - writeFileSync(filePath, text); |
258 | | - |
259 | | - // convert the URI to file:// URI |
260 | | - const fileUri = `file://${filePath}`; |
261 | | - message.params.textDocument.uri = fileUri; |
262 | | - openDocuments.add(fileUri); |
263 | | - } |
264 | | - |
265 | | - // handle textDocument/didChange messages and update the file in the temporary directory |
266 | | - if (message.method === 'textDocument/didChange' && message.params?.textDocument) { |
267 | | - const uri = message.params.textDocument.uri; |
268 | | - |
269 | | - console.log('textDocument/didChange received, URI:', uri); |
270 | | - |
271 | | - // ensure the document is open |
272 | | - const fileUri = ensureDocumentOpen(uri); |
273 | | - |
274 | | - const fileName = uri.replace(/^file:\/\//, ''); |
275 | | - const filePath = join(workspaceDir, fileName); |
276 | | - |
277 | | - // apply the changes to the file |
278 | | - if (message.params.contentChanges && message.params.contentChanges.length > 0) { |
279 | | - console.log('Content changes:', message.params.contentChanges.length); |
280 | | - |
281 | | - // read current file content |
282 | | - let currentContent = ''; |
283 | | - if (existsSync(filePath)) { |
284 | | - currentContent = readFileSync(filePath, 'utf-8'); |
285 | | - } |
286 | | - |
287 | | - // apply all changes sequentially |
288 | | - let updatedContent = currentContent; |
289 | | - for (const change of message.params.contentChanges) { |
290 | | - console.log('Applying change:', change.range ? `range ${change.range.start.line}:${change.range.start.character}-${change.range.end.line}:${change.range.end.character}` : 'full text'); |
291 | | - updatedContent = applyChange(updatedContent, change); |
292 | | - } |
293 | | - |
294 | | - // write the updated content |
295 | | - console.log('Updating file:', filePath); |
296 | | - console.log('updatedContent: ', updatedContent); |
297 | | - writeFileSync(filePath, updatedContent); |
298 | | - } |
299 | | - |
300 | | - // convert the URI to file:// URI |
301 | | - message.params.textDocument.uri = fileUri; |
302 | | - } |
303 | | - |
304 | | - // handle other textDocument requests (hover, completion, etc.) - ensure document is open |
305 | | - if (message.method && message.method.startsWith('textDocument/') && |
306 | | - message.method !== 'textDocument/didOpen' && |
307 | | - message.method !== 'textDocument/didChange' && |
308 | | - message.method !== 'textDocument/didClose' && |
309 | | - message.params?.textDocument?.uri) { |
310 | | - const uri = message.params.textDocument.uri; |
311 | | - console.log(`Ensuring document is open for ${message.method}:`, uri); |
312 | | - const fileUri = ensureDocumentOpen(uri); |
313 | | - message.params.textDocument.uri = fileUri; |
314 | | - } |
315 | | - |
316 | | - // send the converted message to clangd |
317 | | - if (messageHandler) { |
318 | | - messageHandler(JSON.stringify(message)); |
319 | | - } |
320 | | - } catch (err) { |
321 | | - console.error('Error processing message:', err); |
322 | | - // if there is a JSON parsing error, forward the original data |
323 | | - if (messageHandler) { |
324 | | - messageHandler(data); |
325 | | - } |
326 | | - } |
327 | | - }; |
328 | | - }, |
329 | | - onError: (cb) => { |
330 | | - connection.socket.onerror = (event) => { |
331 | | - if ('message' in event) { |
332 | | - cb((event as any).message); |
333 | | - } |
334 | | - }; |
335 | | - }, |
336 | | - onClose: (cb) => { |
337 | | - connection.socket.onclose = (event) => { |
338 | | - cb(event.code, event.reason); |
339 | | - }; |
340 | | - }, |
341 | | - dispose: () => { |
342 | | - connection.socket.close(); |
343 | | - } |
344 | | - }; |
345 | | - |
346 | | - let newConnection = rpcServer.createWebSocketConnection(interceptedSocket); |
347 | | - |
348 | | - rpcServer.forward(newConnection, localConnection); |
349 | | - console.log(`Forwarding new client, workspace: ${workspaceDir}`); |
350 | | - |
351 | | - interceptedSocket.onClose((code, reason) => { |
352 | | - console.log('Client closed', code, reason); |
353 | | - try { |
354 | | - localConnection.dispose(); |
355 | | - // delete the temporary directory |
356 | | - rmSync(workspaceDir, { recursive: true, force: true }); |
357 | | - console.log('Cleaned up workspace directory:', workspaceDir); |
358 | | - } catch (err) { |
359 | | - console.error('Error cleaning up:', err); |
360 | | - } |
| 89 | + handleCLanguageServer(connection, { |
| 90 | + tempDir: tempDir, |
| 91 | + compileCommandsDir: '/etc/clangd' |
361 | 92 | }); |
362 | 93 | }) |
363 | 94 |
|
|
0 commit comments