Skip to content

Commit 76b25ec

Browse files
committed
refactor
1 parent c39849a commit 76b25ec

File tree

2 files changed

+280
-274
lines changed

2 files changed

+280
-274
lines changed

compiler-api/src/index.ts

Lines changed: 5 additions & 274 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
11
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";
43
import fastifyCors from 'fastify-cors';
54
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';
95
import { build_project as build_c_project, requestBodySchema as requestCBodySchema, RequestBody as RequestCBody } from './chooks';
106
import { build_project as build_js_project, requestBodySchema as requestJSBodySchema, RequestBody as RequestJSBody } from './jshooks';
7+
import { handleCLanguageServer } from './language-server/c';
118

129
const server = fastify();
1310

@@ -88,276 +85,10 @@ server.get('/', async (req, reply) => {
8885
reply.code(200).send('ok')
8986
})
9087

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-
10588
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'
36192
});
36293
})
36394

0 commit comments

Comments
 (0)