From c7e8e072e3762ad2e0f30ac362083c229c4bb672 Mon Sep 17 00:00:00 2001 From: RohitKushvaha01 Date: Thu, 31 Jul 2025 18:11:31 +0530 Subject: [PATCH 01/18] lspClient --- .vscode/settings.json | 3 +- src/lib/lspClient.js | 428 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 430 insertions(+), 1 deletion(-) create mode 100644 src/lib/lspClient.js diff --git a/.vscode/settings.json b/.vscode/settings.json index ad71d61d3..44a01d9d8 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -372,5 +372,6 @@ "wxss", "xquery", "Zeek" - ] + ], + "discord.enabled": true } \ No newline at end of file diff --git a/src/lib/lspClient.js b/src/lib/lspClient.js new file mode 100644 index 000000000..3e999edb6 --- /dev/null +++ b/src/lib/lspClient.js @@ -0,0 +1,428 @@ +class lspClient { + constructor(wsUrl) { + this.wsUrl = wsUrl; + this.ws = null; + this.messageId = 0; + this.pendingRequests = new Map(); + this.isInitialized = false; + this.editor = null; + this.currentLanguage = 'javascript'; + this.documentUri = 'file:///example.js'; + this.documentVersion = 0; + this.currentMarkers = []; + } + + connect() { + return new Promise((resolve, reject) => { + try { + this.ws = new WebSocket(this.wsUrl); + + this.ws.onopen = () => { + console.log('WebSocket connected'); + this.initialize().then(resolve).catch(reject); + }; + + this.ws.onmessage = (event) => { + this.handleMessage(JSON.parse(event.data)); + }; + + this.ws.onclose = () => { + console.log('WebSocket closed'); + this.updateStatus('disconnected', 'Disconnected from LSP server'); + this.isInitialized = false; + }; + + this.ws.onerror = (error) => { + console.error('WebSocket error:', error); + this.updateStatus('error', 'WebSocket connection error'); + reject(error); + }; + } catch (error) { + reject(error); + } + }); + } + + disconnect() { + if (this.ws) { + this.ws.close(); + this.ws = null; + this.isInitialized = false; + } + + // Clear diagnostics when disconnecting + if (this.editor) { + this.editor.getSession().clearAnnotations(); + this.clearMarkers(); + } + } + + async initialize() { + const initParams = { + processId: null, + clientInfo: { + name: "ace-lsp-client", + version: "1.0.0" + }, + rootUri: null, + capabilities: { + textDocument: { + synchronization: { + dynamicRegistration: false, + willSave: false, + willSaveWaitUntil: false, + didSave: false + }, + completion: { + dynamicRegistration: false, + completionItem: { + snippetSupport: true, + commitCharactersSupport: true + } + }, + hover: { + dynamicRegistration: false, + contentFormat: ["markdown", "plaintext"] + }, + publishDiagnostics: { + relatedInformation: true, + tagSupport: { + valueSet: [1, 2] + } + } + }, + workspace: { + workspaceFolders: false, + configuration: false + } + } + }; + + try { + await this.sendRequest('initialize', initParams); + await this.sendNotification('initialized', {}); + this.isInitialized = true; + this.updateStatus('connected', 'Connected to LSP server'); + + // Open the document + await this.didOpen(); + } catch (error) { + throw new Error(`Failed to initialize LSP: ${error.message}`); + } + } + + sendRequest(method, params) { + return new Promise((resolve, reject) => { + if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { + reject(new Error('WebSocket not connected')); + return; + } + + const id = ++this.messageId; + const message = { + jsonrpc: '2.0', + id: id, + method: method, + params: params + }; + + this.pendingRequests.set(id, { resolve, reject }); + this.ws.send(JSON.stringify(message)); + + // Timeout after 30 seconds + setTimeout(() => { + if (this.pendingRequests.has(id)) { + this.pendingRequests.delete(id); + reject(new Error('Request timeout')); + } + }, 30000); + }); + } + + sendNotification(method, params) { + if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { + console.warn('Cannot send notification: WebSocket not connected'); + return; + } + + const message = { + jsonrpc: '2.0', + method: method, + params: params + }; + + this.ws.send(JSON.stringify(message)); + } + + handleMessage(message) { + if (message.id && this.pendingRequests.has(message.id)) { + const { resolve, reject } = this.pendingRequests.get(message.id); + this.pendingRequests.delete(message.id); + + if (message.error) { + reject(new Error(message.error.message)); + } else { + resolve(message.result); + } + } else if (message.method) { + this.handleNotification(message.method, message.params); + } + } + + handleNotification(method, params) { + switch (method) { + case 'textDocument/publishDiagnostics': + this.handleDiagnostics(params); + break; + default: + console.log('Unhandled notification:', method, params); + } + } + + handleDiagnostics(params) { + const diagnosticsDiv = document.getElementById('diagnosticsList'); + + // Clear existing annotations and markers + this.editor.getSession().clearAnnotations(); + this.clearMarkers(); + + if (!params.diagnostics || params.diagnostics.length === 0) { + diagnosticsDiv.innerHTML = 'No diagnostics available'; + return; + } + + const annotations = []; + const markers = []; + let html = ''; + + params.diagnostics.forEach(diagnostic => { + const severity = ['', 'error', 'warning', 'info', 'hint'][diagnostic.severity] || 'info'; + const line = diagnostic.range.start.line; + const col = diagnostic.range.start.character; + const endLine = diagnostic.range.end.line; + const endCol = diagnostic.range.end.character; + + // Add annotation for gutter icons (optional, you can remove this if you only want squiggly lines) + annotations.push({ + row: line, + column: col, + text: diagnostic.message, + type: severity === 'hint' ? 'info' : severity + }); + + // Add marker for squiggly underlines with LSP-specific classes + const Range = ace.require('ace/range').Range; + const range = new Range(line, col, endLine, endCol); + const markerId = this.editor.getSession().addMarker( + range, + `ace_lsp_${severity}`, + 'text' + ); + markers.push(markerId); + + html += ` +
+ Line ${line + 1}:${col + 1} - ${diagnostic.message} +
+ `; + }); + + // Set annotations (gutter icons) - comment out this line if you don't want gutter icons + this.editor.getSession().setAnnotations(annotations); + + // Store marker IDs for cleanup + this.currentMarkers = markers; + + diagnosticsDiv.innerHTML = html; + } + + clearMarkers() { + if (this.currentMarkers) { + this.currentMarkers.forEach(markerId => { + this.editor.getSession().removeMarker(markerId); + }); + this.currentMarkers = []; + } + } + + async didOpen() { + if (!this.isInitialized) return; + + const params = { + textDocument: { + uri: this.documentUri, + languageId: this.currentLanguage, + version: ++this.documentVersion, + text: this.editor.getValue() + } + }; + + this.sendNotification('textDocument/didOpen', params); + } + + async didChange(changes) { + if (!this.isInitialized) return; + + const params = { + textDocument: { + uri: this.documentUri, + version: ++this.documentVersion + }, + contentChanges: [{ + text: this.editor.getValue() + }] + }; + + this.sendNotification('textDocument/didChange', params); + } + + async getCompletions(position) { + if (!this.isInitialized) return []; + + try { + const params = { + textDocument: { uri: this.documentUri }, + position: { + line: position.row, + character: position.column + } + }; + + const result = await this.sendRequest('textDocument/completion', params); + const items = result?.items || result || []; + + return items.map(item => ({ + name: item.label, + value: item.insertText || item.label, + meta: item.kind ? this.getCompletionKindName(item.kind) : 'unknown', + docHTML: item.documentation + })); + } catch (error) { + console.warn('Completion request failed:', error); + return []; + } + } + + getCompletionKindName(kind) { + const kinds = { + 1: 'text', 2: 'method', 3: 'function', 4: 'constructor', + 5: 'field', 6: 'variable', 7: 'class', 8: 'interface', + 9: 'module', 10: 'property', 11: 'unit', 12: 'value', + 13: 'enum', 14: 'keyword', 15: 'snippet', 16: 'color', + 17: 'file', 18: 'reference' + }; + return kinds[kind] || 'unknown'; + } + + setEditor(editor) { + this.editor = editor; + } + + setLanguage(language) { + this.currentLanguage = language; + // Update document URI based on language + const extensions = { + javascript: '.js', + typescript: '.ts', + python: '.py', + java: '.java' + }; + this.documentUri = `file:///example${extensions[language] || '.txt'}`; + } + + updateStatus(type, message) { + const statusDiv = document.getElementById('status'); + statusDiv.className = `status ${type}`; + statusDiv.textContent = message; + } +} + +// Initialize Ace Editor +const editor = ace.edit("editor"); +editor.setTheme("ace/theme/monokai"); +editor.session.setMode("ace/mode/javascript"); +editor.setOptions({ + enableBasicAutocompletion: true, + enableLiveAutocompletion: true, + enableSnippets: true, + fontSize: 14 +}); + +// Initialize LSP Client +let lspClient = null; + +// Custom completer for LSP +const lspCompleter = { + getCompletions: async function (editor, session, pos, prefix, callback) { + if (!lspClient || !lspClient.isInitialized) { + callback(null, []); + return; + } + + try { + const completions = await lspClient.getCompletions(pos); + callback(null, completions); + } catch (error) { + console.warn('LSP completion error:', error); + callback(null, []); + } + } +}; + +// Add LSP completer +editor.completers = [lspCompleter]; + +// Handle text changes +let changeTimeout; +editor.on('change', () => { + clearTimeout(changeTimeout); + changeTimeout = setTimeout(() => { + if (lspClient && lspClient.isInitialized) { + lspClient.didChange(); + } + }, 500); // Debounce for 500ms +}); + +// Event handlers +document.getElementById('connectBtn').addEventListener('click', async () => { + const wsUrl = document.getElementById('wsUrl').value; + if (!wsUrl) { + alert('Please enter a WebSocket URL'); + return; + } + + try { + document.getElementById('connectBtn').disabled = true; + lspClient = new LSPClient(wsUrl); + lspClient.setEditor(editor); + lspClient.setLanguage(document.getElementById('languageSelect').value); + + await lspClient.connect(); + + document.getElementById('disconnectBtn').disabled = false; + } catch (error) { + alert(`Failed to connect: ${error.message}`); + document.getElementById('connectBtn').disabled = false; + lspClient = null; + } +}); + +document.getElementById('disconnectBtn').addEventListener('click', () => { + if (lspClient) { + lspClient.disconnect(); + lspClient = null; + } + document.getElementById('connectBtn').disabled = false; + document.getElementById('disconnectBtn').disabled = true; +}); + +document.getElementById('languageSelect').addEventListener('change', (e) => { + const language = e.target.value; + editor.session.setMode(`ace/mode/${language}`); + + if (lspClient) { + lspClient.setLanguage(language); + // Reopen document with new language + lspClient.didOpen(); + } +}); \ No newline at end of file From 82fb53f37b8d010cd65ab758a13a7be56ae5fe58 Mon Sep 17 00:00:00 2001 From: RohitKushvaha01 Date: Thu, 31 Jul 2025 19:15:36 +0530 Subject: [PATCH 02/18] lspClient --- src/lib/LSPClient.js | 158 ++++++++++++++++ src/lib/lspClient.js | 428 ------------------------------------------- 2 files changed, 158 insertions(+), 428 deletions(-) create mode 100644 src/lib/LSPClient.js delete mode 100644 src/lib/lspClient.js diff --git a/src/lib/LSPClient.js b/src/lib/LSPClient.js new file mode 100644 index 000000000..aeed18320 --- /dev/null +++ b/src/lib/LSPClient.js @@ -0,0 +1,158 @@ +class LSPClient { + constructor({ serverUrl, editor, documentUri, language }) { + this.editor = editor; + this.documentUri = documentUri; + this.serverUrl = serverUrl; + this.ws = null; + this.messageId = 0; + this.pendingRequests = new Map(); + this.documentVersion = 1; + this.currentLanguage = language; + } + + + // Establish WebSocket connection and initialize LSP + connect() { + this.ws = new WebSocket(this.serverUrl); + this.ws.onopen = () => { + this.initializeLSP(); + }; + this.ws.onmessage = (event) => this.handleMessage(event); + this.ws.onerror = (error) => console.error('WebSocket error:', error); + this.ws.onclose = () => console.log('WebSocket closed'); + + // Listen to editor changes + this.editor.getSession().on('change', (delta) => { + this.sendDidChange(); + }); + + // Add LSP completer for autocompletion + this.editor.completers = this.editor.completers || []; + this.editor.completers.push({ + getCompletions: (editor, session, pos, prefix, callback) => { + this.requestCompletions(pos, prefix, callback); + } + }); + } + + // Disconnect from the LSP server + disconnect() { + if (this.ws) { + this.ws.close(); + } + } + + // Send initialize request to LSP server + initializeLSP() { + const initParams = { + processId: null, + clientInfo: { name: 'ace-lsp-client' }, + capabilities: { + textDocument: { + completion: { dynamicRegistration: false }, + publishDiagnostics: { relatedInformation: true } + } + } + }; + this.sendRequest('initialize', initParams).then((result) => { + this.sendNotification('initialized', {}); + this.sendDidOpen(); + }).catch((error) => console.error('Initialization failed:', error)); + } + + // Send textDocument/didOpen notification + sendDidOpen() { + const params = { + textDocument: { + uri: this.documentUri, + languageId: this.currentLanguage, + version: this.documentVersion, + text: this.editor.getValue() + } + }; + this.sendNotification('textDocument/didOpen', params); + } + + // Send textDocument/didChange notification + sendDidChange() { + const params = { + textDocument: { + uri: this.documentUri, + version: ++this.documentVersion + }, + contentChanges: [{ text: this.editor.getValue() }] + }; + this.sendNotification('textDocument/didChange', params); + } + + // Request completions from LSP server + requestCompletions(position, prefix, callback) { + const params = { + textDocument: { uri: this.documentUri }, + position: { line: position.row, character: position.column } + }; + this.sendRequest('textDocument/completion', params).then((result) => { + const completions = (result?.items || []).map(item => ({ + caption: item.label, + value: item.insertText || item.label, + meta: item.detail || 'completion' + })); + callback(null, completions); + }).catch((error) => { + console.error('Completion failed:', error); + callback(null, []); + }); + } + + // Send a request and return a promise for the response + sendRequest(method, params) { + return new Promise((resolve, reject) => { + const id = ++this.messageId; + const message = { jsonrpc: '2.0', id, method, params }; + this.pendingRequests.set(id, { resolve, reject }); + this.ws.send(JSON.stringify(message)); + }); + } + + // Send a notification (no response expected) + sendNotification(method, params) { + const message = { jsonrpc: '2.0', method, params }; + this.ws.send(JSON.stringify(message)); + } + + // Handle incoming WebSocket messages + handleMessage(event) { + const message = JSON.parse(event.data); + if (message.id && this.pendingRequests.has(message.id)) { + const { resolve, reject } = this.pendingRequests.get(message.id); + if (message.error) { + reject(message.error); + } else { + resolve(message.result); + } + this.pendingRequests.delete(message.id); + } else if (message.method === 'textDocument/publishDiagnostics') { + this.handleDiagnostics(message.params); + } + } + + // Handle diagnostics from LSP server and display in editor + handleDiagnostics(params) { + const diagnostics = params.diagnostics || []; + const annotations = diagnostics.map(d => ({ + row: d.range.start.line, + column: d.range.start.character, + text: d.message, + type: d.severity === 1 ? 'error' : 'warning' + })); + this.editor.getSession().setAnnotations(annotations); + + // Optional: Update diagnostics list in HTML (assumes element exists) + const diagnosticsList = document.getElementById('diagnosticsList'); + if (diagnosticsList) { + diagnosticsList.innerHTML = diagnostics.map(d => + `
  • ${d.message} at line ${d.range.start.line + 1}
  • ` + ).join(''); + } + } +} \ No newline at end of file diff --git a/src/lib/lspClient.js b/src/lib/lspClient.js deleted file mode 100644 index 3e999edb6..000000000 --- a/src/lib/lspClient.js +++ /dev/null @@ -1,428 +0,0 @@ -class lspClient { - constructor(wsUrl) { - this.wsUrl = wsUrl; - this.ws = null; - this.messageId = 0; - this.pendingRequests = new Map(); - this.isInitialized = false; - this.editor = null; - this.currentLanguage = 'javascript'; - this.documentUri = 'file:///example.js'; - this.documentVersion = 0; - this.currentMarkers = []; - } - - connect() { - return new Promise((resolve, reject) => { - try { - this.ws = new WebSocket(this.wsUrl); - - this.ws.onopen = () => { - console.log('WebSocket connected'); - this.initialize().then(resolve).catch(reject); - }; - - this.ws.onmessage = (event) => { - this.handleMessage(JSON.parse(event.data)); - }; - - this.ws.onclose = () => { - console.log('WebSocket closed'); - this.updateStatus('disconnected', 'Disconnected from LSP server'); - this.isInitialized = false; - }; - - this.ws.onerror = (error) => { - console.error('WebSocket error:', error); - this.updateStatus('error', 'WebSocket connection error'); - reject(error); - }; - } catch (error) { - reject(error); - } - }); - } - - disconnect() { - if (this.ws) { - this.ws.close(); - this.ws = null; - this.isInitialized = false; - } - - // Clear diagnostics when disconnecting - if (this.editor) { - this.editor.getSession().clearAnnotations(); - this.clearMarkers(); - } - } - - async initialize() { - const initParams = { - processId: null, - clientInfo: { - name: "ace-lsp-client", - version: "1.0.0" - }, - rootUri: null, - capabilities: { - textDocument: { - synchronization: { - dynamicRegistration: false, - willSave: false, - willSaveWaitUntil: false, - didSave: false - }, - completion: { - dynamicRegistration: false, - completionItem: { - snippetSupport: true, - commitCharactersSupport: true - } - }, - hover: { - dynamicRegistration: false, - contentFormat: ["markdown", "plaintext"] - }, - publishDiagnostics: { - relatedInformation: true, - tagSupport: { - valueSet: [1, 2] - } - } - }, - workspace: { - workspaceFolders: false, - configuration: false - } - } - }; - - try { - await this.sendRequest('initialize', initParams); - await this.sendNotification('initialized', {}); - this.isInitialized = true; - this.updateStatus('connected', 'Connected to LSP server'); - - // Open the document - await this.didOpen(); - } catch (error) { - throw new Error(`Failed to initialize LSP: ${error.message}`); - } - } - - sendRequest(method, params) { - return new Promise((resolve, reject) => { - if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { - reject(new Error('WebSocket not connected')); - return; - } - - const id = ++this.messageId; - const message = { - jsonrpc: '2.0', - id: id, - method: method, - params: params - }; - - this.pendingRequests.set(id, { resolve, reject }); - this.ws.send(JSON.stringify(message)); - - // Timeout after 30 seconds - setTimeout(() => { - if (this.pendingRequests.has(id)) { - this.pendingRequests.delete(id); - reject(new Error('Request timeout')); - } - }, 30000); - }); - } - - sendNotification(method, params) { - if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { - console.warn('Cannot send notification: WebSocket not connected'); - return; - } - - const message = { - jsonrpc: '2.0', - method: method, - params: params - }; - - this.ws.send(JSON.stringify(message)); - } - - handleMessage(message) { - if (message.id && this.pendingRequests.has(message.id)) { - const { resolve, reject } = this.pendingRequests.get(message.id); - this.pendingRequests.delete(message.id); - - if (message.error) { - reject(new Error(message.error.message)); - } else { - resolve(message.result); - } - } else if (message.method) { - this.handleNotification(message.method, message.params); - } - } - - handleNotification(method, params) { - switch (method) { - case 'textDocument/publishDiagnostics': - this.handleDiagnostics(params); - break; - default: - console.log('Unhandled notification:', method, params); - } - } - - handleDiagnostics(params) { - const diagnosticsDiv = document.getElementById('diagnosticsList'); - - // Clear existing annotations and markers - this.editor.getSession().clearAnnotations(); - this.clearMarkers(); - - if (!params.diagnostics || params.diagnostics.length === 0) { - diagnosticsDiv.innerHTML = 'No diagnostics available'; - return; - } - - const annotations = []; - const markers = []; - let html = ''; - - params.diagnostics.forEach(diagnostic => { - const severity = ['', 'error', 'warning', 'info', 'hint'][diagnostic.severity] || 'info'; - const line = diagnostic.range.start.line; - const col = diagnostic.range.start.character; - const endLine = diagnostic.range.end.line; - const endCol = diagnostic.range.end.character; - - // Add annotation for gutter icons (optional, you can remove this if you only want squiggly lines) - annotations.push({ - row: line, - column: col, - text: diagnostic.message, - type: severity === 'hint' ? 'info' : severity - }); - - // Add marker for squiggly underlines with LSP-specific classes - const Range = ace.require('ace/range').Range; - const range = new Range(line, col, endLine, endCol); - const markerId = this.editor.getSession().addMarker( - range, - `ace_lsp_${severity}`, - 'text' - ); - markers.push(markerId); - - html += ` -
    - Line ${line + 1}:${col + 1} - ${diagnostic.message} -
    - `; - }); - - // Set annotations (gutter icons) - comment out this line if you don't want gutter icons - this.editor.getSession().setAnnotations(annotations); - - // Store marker IDs for cleanup - this.currentMarkers = markers; - - diagnosticsDiv.innerHTML = html; - } - - clearMarkers() { - if (this.currentMarkers) { - this.currentMarkers.forEach(markerId => { - this.editor.getSession().removeMarker(markerId); - }); - this.currentMarkers = []; - } - } - - async didOpen() { - if (!this.isInitialized) return; - - const params = { - textDocument: { - uri: this.documentUri, - languageId: this.currentLanguage, - version: ++this.documentVersion, - text: this.editor.getValue() - } - }; - - this.sendNotification('textDocument/didOpen', params); - } - - async didChange(changes) { - if (!this.isInitialized) return; - - const params = { - textDocument: { - uri: this.documentUri, - version: ++this.documentVersion - }, - contentChanges: [{ - text: this.editor.getValue() - }] - }; - - this.sendNotification('textDocument/didChange', params); - } - - async getCompletions(position) { - if (!this.isInitialized) return []; - - try { - const params = { - textDocument: { uri: this.documentUri }, - position: { - line: position.row, - character: position.column - } - }; - - const result = await this.sendRequest('textDocument/completion', params); - const items = result?.items || result || []; - - return items.map(item => ({ - name: item.label, - value: item.insertText || item.label, - meta: item.kind ? this.getCompletionKindName(item.kind) : 'unknown', - docHTML: item.documentation - })); - } catch (error) { - console.warn('Completion request failed:', error); - return []; - } - } - - getCompletionKindName(kind) { - const kinds = { - 1: 'text', 2: 'method', 3: 'function', 4: 'constructor', - 5: 'field', 6: 'variable', 7: 'class', 8: 'interface', - 9: 'module', 10: 'property', 11: 'unit', 12: 'value', - 13: 'enum', 14: 'keyword', 15: 'snippet', 16: 'color', - 17: 'file', 18: 'reference' - }; - return kinds[kind] || 'unknown'; - } - - setEditor(editor) { - this.editor = editor; - } - - setLanguage(language) { - this.currentLanguage = language; - // Update document URI based on language - const extensions = { - javascript: '.js', - typescript: '.ts', - python: '.py', - java: '.java' - }; - this.documentUri = `file:///example${extensions[language] || '.txt'}`; - } - - updateStatus(type, message) { - const statusDiv = document.getElementById('status'); - statusDiv.className = `status ${type}`; - statusDiv.textContent = message; - } -} - -// Initialize Ace Editor -const editor = ace.edit("editor"); -editor.setTheme("ace/theme/monokai"); -editor.session.setMode("ace/mode/javascript"); -editor.setOptions({ - enableBasicAutocompletion: true, - enableLiveAutocompletion: true, - enableSnippets: true, - fontSize: 14 -}); - -// Initialize LSP Client -let lspClient = null; - -// Custom completer for LSP -const lspCompleter = { - getCompletions: async function (editor, session, pos, prefix, callback) { - if (!lspClient || !lspClient.isInitialized) { - callback(null, []); - return; - } - - try { - const completions = await lspClient.getCompletions(pos); - callback(null, completions); - } catch (error) { - console.warn('LSP completion error:', error); - callback(null, []); - } - } -}; - -// Add LSP completer -editor.completers = [lspCompleter]; - -// Handle text changes -let changeTimeout; -editor.on('change', () => { - clearTimeout(changeTimeout); - changeTimeout = setTimeout(() => { - if (lspClient && lspClient.isInitialized) { - lspClient.didChange(); - } - }, 500); // Debounce for 500ms -}); - -// Event handlers -document.getElementById('connectBtn').addEventListener('click', async () => { - const wsUrl = document.getElementById('wsUrl').value; - if (!wsUrl) { - alert('Please enter a WebSocket URL'); - return; - } - - try { - document.getElementById('connectBtn').disabled = true; - lspClient = new LSPClient(wsUrl); - lspClient.setEditor(editor); - lspClient.setLanguage(document.getElementById('languageSelect').value); - - await lspClient.connect(); - - document.getElementById('disconnectBtn').disabled = false; - } catch (error) { - alert(`Failed to connect: ${error.message}`); - document.getElementById('connectBtn').disabled = false; - lspClient = null; - } -}); - -document.getElementById('disconnectBtn').addEventListener('click', () => { - if (lspClient) { - lspClient.disconnect(); - lspClient = null; - } - document.getElementById('connectBtn').disabled = false; - document.getElementById('disconnectBtn').disabled = true; -}); - -document.getElementById('languageSelect').addEventListener('change', (e) => { - const language = e.target.value; - editor.session.setMode(`ace/mode/${language}`); - - if (lspClient) { - lspClient.setLanguage(language); - // Reopen document with new language - lspClient.didOpen(); - } -}); \ No newline at end of file From d1aeeac3232fd9fcca033af297865099d6594364 Mon Sep 17 00:00:00 2001 From: RohitKushvaha01 Date: Thu, 31 Jul 2025 20:11:26 +0530 Subject: [PATCH 03/18] feat. add lspClient to acode.js --- package-lock.json | 122 +------------------------ package.json | 2 +- src/lib/acode.js | 4 +- src/lib/{LSPClient.js => lspClient.js} | 2 +- www/index.html | 10 +- 5 files changed, 12 insertions(+), 128 deletions(-) rename src/lib/{LSPClient.js => lspClient.js} (99%) diff --git a/package-lock.json b/package-lock.json index 80400ab01..0fa28bd8f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10149,126 +10149,6 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, - "node_modules/ts-loader": { - "version": "9.5.2", - "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.2.tgz", - "integrity": "sha512-Qo4piXvOTWcMGIgRiuFa6nHNm+54HbYaZCKqc9eeZCLRy3XqafQgwX2F7mofrbJG3g7EEb+lkiR+z2Lic2s3Zw==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.1.0", - "enhanced-resolve": "^5.0.0", - "micromatch": "^4.0.0", - "semver": "^7.3.4", - "source-map": "^0.7.4" - }, - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "typescript": "*", - "webpack": "^5.0.0" - } - }, - "node_modules/ts-loader/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/ts-loader/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/ts-loader/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/ts-loader/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/ts-loader/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/ts-loader/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/ts-loader/node_modules/source-map": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", - "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">= 8" - } - }, - "node_modules/ts-loader/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/tslib": { "version": "1.14.1", "license": "0BSD" @@ -10324,6 +10204,8 @@ "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "dev": true, "license": "Apache-2.0", + "optional": true, + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/package.json b/package.json index f29ca0c5c..cb7df5654 100644 --- a/package.json +++ b/package.json @@ -129,4 +129,4 @@ "yargs": "^17.7.2" }, "browserslist": "cover 100%,not android < 5" -} +} \ No newline at end of file diff --git a/src/lib/acode.js b/src/lib/acode.js index f278cf142..bb37c69b7 100644 --- a/src/lib/acode.js +++ b/src/lib/acode.js @@ -44,6 +44,7 @@ import helpers from "utils/helpers"; import KeyboardEvent from "utils/keyboardEvent"; import Url from "utils/Url"; import constants from "./constants"; +import lspClient from "./lspClient"; export default class Acode { #modules = {}; @@ -78,7 +79,7 @@ export default class Acode { list: themes.list, update: themes.update, // Deprecated, not supported anymore - apply: () => {}, + apply: () => { }, }; const sidebarAppsModule = { @@ -119,6 +120,7 @@ export default class Acode { }, }; + this.define("lspClient", lspClient); this.define("Url", Url); this.define("page", Page); this.define("Color", Color); diff --git a/src/lib/LSPClient.js b/src/lib/lspClient.js similarity index 99% rename from src/lib/LSPClient.js rename to src/lib/lspClient.js index aeed18320..8c0e4a21a 100644 --- a/src/lib/LSPClient.js +++ b/src/lib/lspClient.js @@ -1,4 +1,4 @@ -class LSPClient { +export default class lspClient { constructor({ serverUrl, editor, documentUri, language }) { this.editor = editor; this.documentUri = documentUri; diff --git a/www/index.html b/www/index.html index e4c0e5ec3..ae9aa7f4d 100644 --- a/www/index.html +++ b/www/index.html @@ -165,17 +165,17 @@ Acode - - - - - + + + + + From 73900e08733249a7e6e3a6ec3ade9899f96d8bba Mon Sep 17 00:00:00 2001 From: RohitKushvaha01 Date: Fri, 1 Aug 2025 09:09:05 +0530 Subject: [PATCH 04/18] feat. allow multiple editors on a single connection --- src/lib/lspClient.js | 208 ++++++++++++++++++++++++++++--------------- 1 file changed, 136 insertions(+), 72 deletions(-) diff --git a/src/lib/lspClient.js b/src/lib/lspClient.js index 8c0e4a21a..0a5826109 100644 --- a/src/lib/lspClient.js +++ b/src/lib/lspClient.js @@ -1,15 +1,13 @@ export default class lspClient { - constructor({ serverUrl, editor, documentUri, language }) { - this.editor = editor; - this.documentUri = documentUri; + constructor({ serverUrl }) { this.serverUrl = serverUrl; this.ws = null; this.messageId = 0; this.pendingRequests = new Map(); - this.documentVersion = 1; - this.currentLanguage = language; - } + // Map of editorId -> { editor, documentUri, language, version, changeHandler } + this.editors = new Map(); + } // Establish WebSocket connection and initialize LSP connect() { @@ -20,29 +18,14 @@ export default class lspClient { this.ws.onmessage = (event) => this.handleMessage(event); this.ws.onerror = (error) => console.error('WebSocket error:', error); this.ws.onclose = () => console.log('WebSocket closed'); - - // Listen to editor changes - this.editor.getSession().on('change', (delta) => { - this.sendDidChange(); - }); - - // Add LSP completer for autocompletion - this.editor.completers = this.editor.completers || []; - this.editor.completers.push({ - getCompletions: (editor, session, pos, prefix, callback) => { - this.requestCompletions(pos, prefix, callback); - } - }); } - // Disconnect from the LSP server disconnect() { if (this.ws) { this.ws.close(); } } - // Send initialize request to LSP server initializeLSP() { const initParams = { processId: null, @@ -54,59 +37,135 @@ export default class lspClient { } } }; - this.sendRequest('initialize', initParams).then((result) => { - this.sendNotification('initialized', {}); - this.sendDidOpen(); - }).catch((error) => console.error('Initialization failed:', error)); + this.sendRequest('initialize', initParams) + .then(() => { + this.sendNotification('initialized', {}); + // Open all already-registered editors + for (const [id, meta] of this.editors) { + this.sendDidOpen(id); + } + }) + .catch((error) => console.error('Initialization failed:', error)); + } + + // Add an editor/tab to share this single connection + addEditor(id, editor, documentUri, language) { + if (this.editors.has(id)) { + console.warn(`Editor with id ${id} already registered; replacing.`); + this.removeEditor(id); + } + + const meta = { + editor, + documentUri, + language, + version: 1, + changeHandler: null, + }; + + // change listener + const changeHandler = () => { + this.sendDidChange(id); + }; + meta.changeHandler = changeHandler; + editor.getSession().on('change', changeHandler); + + // completer for this editor + editor.completers = editor.completers || []; + editor.completers.push({ + getCompletions: (ed, session, pos, prefix, callback) => { + this.requestCompletions(id, pos, prefix, callback); + }, + }); + + this.editors.set(id, meta); + + // If already initialized, immediately send didOpen + this.sendDidOpen(id); + } + + // Remove an editor/tab + removeEditor(id) { + const meta = this.editors.get(id); + if (!meta) return; + const { editor, changeHandler, documentUri } = meta; + + // Optionally notify the server that the document is closed + this.sendNotification('textDocument/didClose', { + textDocument: { uri: documentUri }, + }); + + // Tear down listener + if (changeHandler) { + editor.getSession().removeListener('change', changeHandler); + } + + // Note: removing completer is left to caller if needed + this.editors.delete(id); } - // Send textDocument/didOpen notification - sendDidOpen() { + sendDidOpen(id) { + const meta = this.editors.get(id); + if (!meta) return; + const { editor, documentUri, language, version } = meta; const params = { textDocument: { - uri: this.documentUri, - languageId: this.currentLanguage, - version: this.documentVersion, - text: this.editor.getValue() - } + uri: documentUri, + languageId: language, + version, + text: editor.getValue(), + }, }; this.sendNotification('textDocument/didOpen', params); } - // Send textDocument/didChange notification - sendDidChange() { + sendDidChange(id) { + const meta = this.editors.get(id); + if (!meta) return; + const { editor, documentUri } = meta; + meta.version += 1; const params = { textDocument: { - uri: this.documentUri, - version: ++this.documentVersion + uri: documentUri, + version: meta.version, }, - contentChanges: [{ text: this.editor.getValue() }] + contentChanges: [{ text: editor.getValue() }], }; this.sendNotification('textDocument/didChange', params); } - // Request completions from LSP server - requestCompletions(position, prefix, callback) { + requestCompletions(id, position, prefix, callback) { + const meta = this.editors.get(id); + if (!meta) { + callback(null, []); + return; + } + const { documentUri } = meta; const params = { - textDocument: { uri: this.documentUri }, - position: { line: position.row, character: position.column } + textDocument: { uri: documentUri }, + position: { line: position.row, character: position.column }, }; - this.sendRequest('textDocument/completion', params).then((result) => { - const completions = (result?.items || []).map(item => ({ - caption: item.label, - value: item.insertText || item.label, - meta: item.detail || 'completion' - })); - callback(null, completions); - }).catch((error) => { - console.error('Completion failed:', error); - callback(null, []); - }); + this.sendRequest('textDocument/completion', params) + .then((result) => { + const completions = (result?.items || []).map((item) => ({ + caption: item.label, + value: item.insertText || item.label, + meta: item.detail || 'completion', + })); + callback(null, completions); + }) + .catch((error) => { + console.error('Completion failed:', error); + callback(null, []); + }); } - // Send a request and return a promise for the response sendRequest(method, params) { return new Promise((resolve, reject) => { + if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { + reject(new Error('WebSocket not open')); + return; + } const id = ++this.messageId; const message = { jsonrpc: '2.0', id, method, params }; this.pendingRequests.set(id, { resolve, reject }); @@ -114,15 +173,21 @@ export default class lspClient { }); } - // Send a notification (no response expected) sendNotification(method, params) { + if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return; const message = { jsonrpc: '2.0', method, params }; this.ws.send(JSON.stringify(message)); } - // Handle incoming WebSocket messages handleMessage(event) { - const message = JSON.parse(event.data); + let message; + try { + message = JSON.parse(event.data); + } catch (e) { + console.warn('Failed to parse LSP message', e); + return; + } + if (message.id && this.pendingRequests.has(message.id)) { const { resolve, reject } = this.pendingRequests.get(message.id); if (message.error) { @@ -136,23 +201,22 @@ export default class lspClient { } } - // Handle diagnostics from LSP server and display in editor handleDiagnostics(params) { const diagnostics = params.diagnostics || []; - const annotations = diagnostics.map(d => ({ - row: d.range.start.line, - column: d.range.start.character, - text: d.message, - type: d.severity === 1 ? 'error' : 'warning' - })); - this.editor.getSession().setAnnotations(annotations); - - // Optional: Update diagnostics list in HTML (assumes element exists) - const diagnosticsList = document.getElementById('diagnosticsList'); - if (diagnosticsList) { - diagnosticsList.innerHTML = diagnostics.map(d => - `
  • ${d.message} at line ${d.range.start.line + 1}
  • ` - ).join(''); + const uri = params.uri || (params.textDocument && params.textDocument.uri); + if (!uri) return; + + // Find all editors with that document URI + for (const [, meta] of this.editors) { + if (meta.documentUri === uri) { + const annotations = diagnostics.map((d) => ({ + row: d.range.start.line, + column: d.range.start.character, + text: d.message, + type: d.severity === 1 ? 'error' : 'warning', + })); + meta.editor.getSession().setAnnotations(annotations); + } } } -} \ No newline at end of file +} From a0f9691ecb156cb36e8c1ab9b6cd8bd50e05fe81 Mon Sep 17 00:00:00 2001 From: RohitKushvaha01 Date: Fri, 1 Aug 2025 09:19:32 +0530 Subject: [PATCH 05/18] . --- src/lib/lspClient.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/lspClient.js b/src/lib/lspClient.js index 0a5826109..65c31ecaf 100644 --- a/src/lib/lspClient.js +++ b/src/lib/lspClient.js @@ -1,5 +1,5 @@ export default class lspClient { - constructor({ serverUrl }) { + constructor(serverUrl) { this.serverUrl = serverUrl; this.ws = null; this.messageId = 0; From d5bee4986e88bcea43f5c5d108900a5524735a64 Mon Sep 17 00:00:00 2001 From: RohitKushvaha01 Date: Fri, 1 Aug 2025 09:20:21 +0530 Subject: [PATCH 06/18] format --- src/lib/acode.js | 2 +- src/lib/lspClient.js | 440 +++++++++++++++++++++---------------------- 2 files changed, 221 insertions(+), 221 deletions(-) diff --git a/src/lib/acode.js b/src/lib/acode.js index bb37c69b7..1e548bea1 100644 --- a/src/lib/acode.js +++ b/src/lib/acode.js @@ -79,7 +79,7 @@ export default class Acode { list: themes.list, update: themes.update, // Deprecated, not supported anymore - apply: () => { }, + apply: () => {}, }; const sidebarAppsModule = { diff --git a/src/lib/lspClient.js b/src/lib/lspClient.js index 65c31ecaf..6892ad146 100644 --- a/src/lib/lspClient.js +++ b/src/lib/lspClient.js @@ -1,222 +1,222 @@ export default class lspClient { - constructor(serverUrl) { - this.serverUrl = serverUrl; - this.ws = null; - this.messageId = 0; - this.pendingRequests = new Map(); - - // Map of editorId -> { editor, documentUri, language, version, changeHandler } - this.editors = new Map(); - } - - // Establish WebSocket connection and initialize LSP - connect() { - this.ws = new WebSocket(this.serverUrl); - this.ws.onopen = () => { - this.initializeLSP(); - }; - this.ws.onmessage = (event) => this.handleMessage(event); - this.ws.onerror = (error) => console.error('WebSocket error:', error); - this.ws.onclose = () => console.log('WebSocket closed'); - } - - disconnect() { - if (this.ws) { - this.ws.close(); - } - } - - initializeLSP() { - const initParams = { - processId: null, - clientInfo: { name: 'ace-lsp-client' }, - capabilities: { - textDocument: { - completion: { dynamicRegistration: false }, - publishDiagnostics: { relatedInformation: true } - } - } - }; - this.sendRequest('initialize', initParams) - .then(() => { - this.sendNotification('initialized', {}); - // Open all already-registered editors - for (const [id, meta] of this.editors) { - this.sendDidOpen(id); - } - }) - .catch((error) => console.error('Initialization failed:', error)); - } - - // Add an editor/tab to share this single connection - addEditor(id, editor, documentUri, language) { - if (this.editors.has(id)) { - console.warn(`Editor with id ${id} already registered; replacing.`); - this.removeEditor(id); - } - - const meta = { - editor, - documentUri, - language, - version: 1, - changeHandler: null, - }; - - // change listener - const changeHandler = () => { - this.sendDidChange(id); - }; - meta.changeHandler = changeHandler; - editor.getSession().on('change', changeHandler); - - // completer for this editor - editor.completers = editor.completers || []; - editor.completers.push({ - getCompletions: (ed, session, pos, prefix, callback) => { - this.requestCompletions(id, pos, prefix, callback); - }, - }); - - this.editors.set(id, meta); - - // If already initialized, immediately send didOpen - this.sendDidOpen(id); - } - - // Remove an editor/tab - removeEditor(id) { - const meta = this.editors.get(id); - if (!meta) return; - const { editor, changeHandler, documentUri } = meta; - - // Optionally notify the server that the document is closed - this.sendNotification('textDocument/didClose', { - textDocument: { uri: documentUri }, - }); - - // Tear down listener - if (changeHandler) { - editor.getSession().removeListener('change', changeHandler); - } - - // Note: removing completer is left to caller if needed - this.editors.delete(id); - } - - sendDidOpen(id) { - const meta = this.editors.get(id); - if (!meta) return; - const { editor, documentUri, language, version } = meta; - const params = { - textDocument: { - uri: documentUri, - languageId: language, - version, - text: editor.getValue(), - }, - }; - this.sendNotification('textDocument/didOpen', params); - } - - sendDidChange(id) { - const meta = this.editors.get(id); - if (!meta) return; - const { editor, documentUri } = meta; - meta.version += 1; - const params = { - textDocument: { - uri: documentUri, - version: meta.version, - }, - contentChanges: [{ text: editor.getValue() }], - }; - this.sendNotification('textDocument/didChange', params); - } - - requestCompletions(id, position, prefix, callback) { - const meta = this.editors.get(id); - if (!meta) { - callback(null, []); - return; - } - const { documentUri } = meta; - const params = { - textDocument: { uri: documentUri }, - position: { line: position.row, character: position.column }, - }; - this.sendRequest('textDocument/completion', params) - .then((result) => { - const completions = (result?.items || []).map((item) => ({ - caption: item.label, - value: item.insertText || item.label, - meta: item.detail || 'completion', - })); - callback(null, completions); - }) - .catch((error) => { - console.error('Completion failed:', error); - callback(null, []); - }); - } - - sendRequest(method, params) { - return new Promise((resolve, reject) => { - if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { - reject(new Error('WebSocket not open')); - return; - } - const id = ++this.messageId; - const message = { jsonrpc: '2.0', id, method, params }; - this.pendingRequests.set(id, { resolve, reject }); - this.ws.send(JSON.stringify(message)); - }); - } - - sendNotification(method, params) { - if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return; - const message = { jsonrpc: '2.0', method, params }; - this.ws.send(JSON.stringify(message)); - } - - handleMessage(event) { - let message; - try { - message = JSON.parse(event.data); - } catch (e) { - console.warn('Failed to parse LSP message', e); - return; - } - - if (message.id && this.pendingRequests.has(message.id)) { - const { resolve, reject } = this.pendingRequests.get(message.id); - if (message.error) { - reject(message.error); - } else { - resolve(message.result); - } - this.pendingRequests.delete(message.id); - } else if (message.method === 'textDocument/publishDiagnostics') { - this.handleDiagnostics(message.params); - } - } - - handleDiagnostics(params) { - const diagnostics = params.diagnostics || []; - const uri = params.uri || (params.textDocument && params.textDocument.uri); - if (!uri) return; - - // Find all editors with that document URI - for (const [, meta] of this.editors) { - if (meta.documentUri === uri) { - const annotations = diagnostics.map((d) => ({ - row: d.range.start.line, - column: d.range.start.character, - text: d.message, - type: d.severity === 1 ? 'error' : 'warning', - })); - meta.editor.getSession().setAnnotations(annotations); - } - } - } + constructor(serverUrl) { + this.serverUrl = serverUrl; + this.ws = null; + this.messageId = 0; + this.pendingRequests = new Map(); + + // Map of editorId -> { editor, documentUri, language, version, changeHandler } + this.editors = new Map(); + } + + // Establish WebSocket connection and initialize LSP + connect() { + this.ws = new WebSocket(this.serverUrl); + this.ws.onopen = () => { + this.initializeLSP(); + }; + this.ws.onmessage = (event) => this.handleMessage(event); + this.ws.onerror = (error) => console.error("WebSocket error:", error); + this.ws.onclose = () => console.log("WebSocket closed"); + } + + disconnect() { + if (this.ws) { + this.ws.close(); + } + } + + initializeLSP() { + const initParams = { + processId: null, + clientInfo: { name: "ace-lsp-client" }, + capabilities: { + textDocument: { + completion: { dynamicRegistration: false }, + publishDiagnostics: { relatedInformation: true }, + }, + }, + }; + this.sendRequest("initialize", initParams) + .then(() => { + this.sendNotification("initialized", {}); + // Open all already-registered editors + for (const [id, meta] of this.editors) { + this.sendDidOpen(id); + } + }) + .catch((error) => console.error("Initialization failed:", error)); + } + + // Add an editor/tab to share this single connection + addEditor(id, editor, documentUri, language) { + if (this.editors.has(id)) { + console.warn(`Editor with id ${id} already registered; replacing.`); + this.removeEditor(id); + } + + const meta = { + editor, + documentUri, + language, + version: 1, + changeHandler: null, + }; + + // change listener + const changeHandler = () => { + this.sendDidChange(id); + }; + meta.changeHandler = changeHandler; + editor.getSession().on("change", changeHandler); + + // completer for this editor + editor.completers = editor.completers || []; + editor.completers.push({ + getCompletions: (ed, session, pos, prefix, callback) => { + this.requestCompletions(id, pos, prefix, callback); + }, + }); + + this.editors.set(id, meta); + + // If already initialized, immediately send didOpen + this.sendDidOpen(id); + } + + // Remove an editor/tab + removeEditor(id) { + const meta = this.editors.get(id); + if (!meta) return; + const { editor, changeHandler, documentUri } = meta; + + // Optionally notify the server that the document is closed + this.sendNotification("textDocument/didClose", { + textDocument: { uri: documentUri }, + }); + + // Tear down listener + if (changeHandler) { + editor.getSession().removeListener("change", changeHandler); + } + + // Note: removing completer is left to caller if needed + this.editors.delete(id); + } + + sendDidOpen(id) { + const meta = this.editors.get(id); + if (!meta) return; + const { editor, documentUri, language, version } = meta; + const params = { + textDocument: { + uri: documentUri, + languageId: language, + version, + text: editor.getValue(), + }, + }; + this.sendNotification("textDocument/didOpen", params); + } + + sendDidChange(id) { + const meta = this.editors.get(id); + if (!meta) return; + const { editor, documentUri } = meta; + meta.version += 1; + const params = { + textDocument: { + uri: documentUri, + version: meta.version, + }, + contentChanges: [{ text: editor.getValue() }], + }; + this.sendNotification("textDocument/didChange", params); + } + + requestCompletions(id, position, prefix, callback) { + const meta = this.editors.get(id); + if (!meta) { + callback(null, []); + return; + } + const { documentUri } = meta; + const params = { + textDocument: { uri: documentUri }, + position: { line: position.row, character: position.column }, + }; + this.sendRequest("textDocument/completion", params) + .then((result) => { + const completions = (result?.items || []).map((item) => ({ + caption: item.label, + value: item.insertText || item.label, + meta: item.detail || "completion", + })); + callback(null, completions); + }) + .catch((error) => { + console.error("Completion failed:", error); + callback(null, []); + }); + } + + sendRequest(method, params) { + return new Promise((resolve, reject) => { + if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { + reject(new Error("WebSocket not open")); + return; + } + const id = ++this.messageId; + const message = { jsonrpc: "2.0", id, method, params }; + this.pendingRequests.set(id, { resolve, reject }); + this.ws.send(JSON.stringify(message)); + }); + } + + sendNotification(method, params) { + if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return; + const message = { jsonrpc: "2.0", method, params }; + this.ws.send(JSON.stringify(message)); + } + + handleMessage(event) { + let message; + try { + message = JSON.parse(event.data); + } catch (e) { + console.warn("Failed to parse LSP message", e); + return; + } + + if (message.id && this.pendingRequests.has(message.id)) { + const { resolve, reject } = this.pendingRequests.get(message.id); + if (message.error) { + reject(message.error); + } else { + resolve(message.result); + } + this.pendingRequests.delete(message.id); + } else if (message.method === "textDocument/publishDiagnostics") { + this.handleDiagnostics(message.params); + } + } + + handleDiagnostics(params) { + const diagnostics = params.diagnostics || []; + const uri = params.uri || (params.textDocument && params.textDocument.uri); + if (!uri) return; + + // Find all editors with that document URI + for (const [, meta] of this.editors) { + if (meta.documentUri === uri) { + const annotations = diagnostics.map((d) => ({ + row: d.range.start.line, + column: d.range.start.character, + text: d.message, + type: d.severity === 1 ? "error" : "warning", + })); + meta.editor.getSession().setAnnotations(annotations); + } + } + } } From d2fec9f074c9f850725fb5c4512a5f6355b10492 Mon Sep 17 00:00:00 2001 From: RohitKushvaha01 Date: Fri, 1 Aug 2025 11:51:35 +0530 Subject: [PATCH 07/18] ace-linters --- package-lock.json | 43 ++++++++++++ package.json | 1 + src/lib/lspClient.js | 157 ++++++++++++++++++++++--------------------- 3 files changed, 124 insertions(+), 77 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0fa28bd8f..fe78375b6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "@xterm/addon-web-links": "^0.11.0", "@xterm/addon-webgl": "^0.18.0", "@xterm/xterm": "^5.5.0", + "ace-linters": "^1.8.1", "autosize": "^6.0.1", "cordova": "12.0.0", "core-js": "^3.37.1", @@ -3187,6 +3188,17 @@ "node": ">= 0.6" } }, + "node_modules/ace-linters": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/ace-linters/-/ace-linters-1.8.1.tgz", + "integrity": "sha512-gA/go8RjNsSnN1zcgBVQa9Uk81E1bAUSgSC41Wp50axz0L4XipesyPHhL9qImMAg8AnAlFA+i0qJbM/bd2zCWQ==", + "license": "MIT", + "dependencies": { + "vscode-languageserver-protocol": "^3.17.5", + "vscode-languageserver-textdocument": "^1.0.12", + "vscode-languageserver-types": "^3.17.5" + } + }, "node_modules/acorn": { "version": "8.11.3", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", @@ -10433,6 +10445,37 @@ "version": "1.0.2", "license": "MIT" }, + "node_modules/vscode-jsonrpc": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", + "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/vscode-languageserver-protocol": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", + "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==", + "license": "MIT", + "dependencies": { + "vscode-jsonrpc": "8.2.0", + "vscode-languageserver-types": "3.17.5" + } + }, + "node_modules/vscode-languageserver-textdocument": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz", + "integrity": "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==", + "license": "MIT" + }, + "node_modules/vscode-languageserver-types": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", + "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==", + "license": "MIT" + }, "node_modules/walk-up-path": { "version": "3.0.1", "license": "ISC" diff --git a/package.json b/package.json index cb7df5654..9ae134e20 100644 --- a/package.json +++ b/package.json @@ -105,6 +105,7 @@ "@xterm/addon-web-links": "^0.11.0", "@xterm/addon-webgl": "^0.18.0", "@xterm/xterm": "^5.5.0", + "ace-linters": "^1.8.1", "autosize": "^6.0.1", "cordova": "12.0.0", "core-js": "^3.37.1", diff --git a/src/lib/lspClient.js b/src/lib/lspClient.js index 6892ad146..78e11558b 100644 --- a/src/lib/lspClient.js +++ b/src/lib/lspClient.js @@ -1,29 +1,27 @@ +import "ace-linters"; + export default class lspClient { constructor(serverUrl) { this.serverUrl = serverUrl; this.ws = null; this.messageId = 0; this.pendingRequests = new Map(); + this.editors = new Map(); // editorId -> { editor, documentUri, language, version } - // Map of editorId -> { editor, documentUri, language, version, changeHandler } - this.editors = new Map(); + // Enable ace language tools + ace.require("ace/ext/language_tools"); } - // Establish WebSocket connection and initialize LSP connect() { this.ws = new WebSocket(this.serverUrl); - this.ws.onopen = () => { - this.initializeLSP(); - }; + this.ws.onopen = () => this.initializeLSP(); this.ws.onmessage = (event) => this.handleMessage(event); this.ws.onerror = (error) => console.error("WebSocket error:", error); this.ws.onclose = () => console.log("WebSocket closed"); } disconnect() { - if (this.ws) { - this.ws.close(); - } + this.ws?.close(); } initializeLSP() { @@ -37,127 +35,121 @@ export default class lspClient { }, }, }; + this.sendRequest("initialize", initParams) .then(() => { this.sendNotification("initialized", {}); - // Open all already-registered editors - for (const [id, meta] of this.editors) { + // Open all registered editors + for (const [id] of this.editors) { this.sendDidOpen(id); } }) .catch((error) => console.error("Initialization failed:", error)); } - // Add an editor/tab to share this single connection - addEditor(id, editor, documentUri, language) { + addEditor(id, editor, documentUri, language, mode) { if (this.editors.has(id)) { - console.warn(`Editor with id ${id} already registered; replacing.`); this.removeEditor(id); } - const meta = { - editor, - documentUri, - language, - version: 1, - changeHandler: null, - }; + // Setup ace-linters with mode + const session = editor.getSession(); - // change listener - const changeHandler = () => { - this.sendDidChange(id); - }; - meta.changeHandler = changeHandler; - editor.getSession().on("change", changeHandler); + session.setMode(mode); + session.setUseWorker(true); - // completer for this editor + // Enable autocompletion + editor.setOptions({ + enableBasicAutocompletion: true, + enableLiveAutocompletion: true, + enableSnippets: false, + }); + + // Add LSP completer editor.completers = editor.completers || []; editor.completers.push({ getCompletions: (ed, session, pos, prefix, callback) => { - this.requestCompletions(id, pos, prefix, callback); + this.requestCompletions(id, pos, callback); }, }); - this.editors.set(id, meta); + // Track changes for LSP + const changeHandler = () => this.sendDidChange(id); + session.on("change", changeHandler); + + this.editors.set(id, { + editor, + documentUri, + language, + version: 1, + changeHandler, + }); - // If already initialized, immediately send didOpen this.sendDidOpen(id); } - // Remove an editor/tab removeEditor(id) { const meta = this.editors.get(id); if (!meta) return; - const { editor, changeHandler, documentUri } = meta; - // Optionally notify the server that the document is closed + // Clean up + meta.editor.getSession().removeListener("change", meta.changeHandler); this.sendNotification("textDocument/didClose", { - textDocument: { uri: documentUri }, + textDocument: { uri: meta.documentUri }, }); - - // Tear down listener - if (changeHandler) { - editor.getSession().removeListener("change", changeHandler); - } - - // Note: removing completer is left to caller if needed this.editors.delete(id); } sendDidOpen(id) { const meta = this.editors.get(id); if (!meta) return; - const { editor, documentUri, language, version } = meta; - const params = { + + this.sendNotification("textDocument/didOpen", { textDocument: { - uri: documentUri, - languageId: language, - version, - text: editor.getValue(), + uri: meta.documentUri, + languageId: meta.language, + version: meta.version, + text: meta.editor.getValue(), }, - }; - this.sendNotification("textDocument/didOpen", params); + }); } sendDidChange(id) { const meta = this.editors.get(id); if (!meta) return; - const { editor, documentUri } = meta; + meta.version += 1; - const params = { + this.sendNotification("textDocument/didChange", { textDocument: { - uri: documentUri, + uri: meta.documentUri, version: meta.version, }, - contentChanges: [{ text: editor.getValue() }], - }; - this.sendNotification("textDocument/didChange", params); + contentChanges: [{ text: meta.editor.getValue() }], + }); } - requestCompletions(id, position, prefix, callback) { + requestCompletions(id, position, callback) { const meta = this.editors.get(id); if (!meta) { callback(null, []); return; } - const { documentUri } = meta; + const params = { - textDocument: { uri: documentUri }, + textDocument: { uri: meta.documentUri }, position: { line: position.row, character: position.column }, }; + this.sendRequest("textDocument/completion", params) .then((result) => { const completions = (result?.items || []).map((item) => ({ caption: item.label, value: item.insertText || item.label, - meta: item.detail || "completion", + meta: item.detail || "lsp", })); callback(null, completions); }) - .catch((error) => { - console.error("Completion failed:", error); - callback(null, []); - }); + .catch(() => callback(null, [])); } sendRequest(method, params) { @@ -166,6 +158,7 @@ export default class lspClient { reject(new Error("WebSocket not open")); return; } + const id = ++this.messageId; const message = { jsonrpc: "2.0", id, method, params }; this.pendingRequests.set(id, { resolve, reject }); @@ -174,9 +167,10 @@ export default class lspClient { } sendNotification(method, params) { - if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return; - const message = { jsonrpc: "2.0", method, params }; - this.ws.send(JSON.stringify(message)); + if (this.ws?.readyState === WebSocket.OPEN) { + const message = { jsonrpc: "2.0", method, params }; + this.ws.send(JSON.stringify(message)); + } } handleMessage(event) { @@ -188,34 +182,43 @@ export default class lspClient { return; } + // Handle responses if (message.id && this.pendingRequests.has(message.id)) { const { resolve, reject } = this.pendingRequests.get(message.id); - if (message.error) { - reject(message.error); - } else { - resolve(message.result); - } + message.error ? reject(message.error) : resolve(message.result); this.pendingRequests.delete(message.id); - } else if (message.method === "textDocument/publishDiagnostics") { + } + // Handle diagnostics - add to ace-linters annotations + else if (message.method === "textDocument/publishDiagnostics") { this.handleDiagnostics(message.params); } } handleDiagnostics(params) { const diagnostics = params.diagnostics || []; - const uri = params.uri || (params.textDocument && params.textDocument.uri); + const uri = params.uri; if (!uri) return; - // Find all editors with that document URI + // Find editors with matching URI for (const [, meta] of this.editors) { if (meta.documentUri === uri) { - const annotations = diagnostics.map((d) => ({ + const session = meta.editor.getSession(); + + // Get existing ace-linters annotations + const existing = session.getAnnotations() || []; + const linterAnnotations = existing.filter((a) => a.source !== "lsp"); + + // Add LSP diagnostics + const lspAnnotations = diagnostics.map((d) => ({ row: d.range.start.line, column: d.range.start.character, text: d.message, type: d.severity === 1 ? "error" : "warning", + source: "lsp", })); - meta.editor.getSession().setAnnotations(annotations); + + // Combine and set + session.setAnnotations([...linterAnnotations, ...lspAnnotations]); } } } From f7053cd016abab93f647288f7bb137a7c385d76a Mon Sep 17 00:00:00 2001 From: RohitKushvaha01 Date: Tue, 5 Aug 2025 16:24:40 +0530 Subject: [PATCH 08/18] squiggly lines + improvements --- package-lock.json | 1 - src/lib/lspClient.js | 390 ++++++++++++++++++++++--------------------- 2 files changed, 203 insertions(+), 188 deletions(-) diff --git a/package-lock.json b/package-lock.json index bc9902522..fe78375b6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10210,7 +10210,6 @@ "is-typedarray": "^1.0.0" } }, - "node_modules/typescript": { "version": "5.8.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", diff --git a/src/lib/lspClient.js b/src/lib/lspClient.js index 78e11558b..6089c64d4 100644 --- a/src/lib/lspClient.js +++ b/src/lib/lspClient.js @@ -1,225 +1,241 @@ -import "ace-linters"; +import { AceLanguageClient } from "ace-linters/build/ace-language-client"; export default class lspClient { - constructor(serverUrl) { - this.serverUrl = serverUrl; - this.ws = null; - this.messageId = 0; - this.pendingRequests = new Map(); - this.editors = new Map(); // editorId -> { editor, documentUri, language, version } - - // Enable ace language tools - ace.require("ace/ext/language_tools"); + constructor(wsUrl, modes) { + this.wsUrl = wsUrl; + // Convert modes array to string if needed, or keep as string + this.modes = Array.isArray(modes) ? modes.join(',') : modes; + this.socket = null; + this.languageProvider = null; + this.registeredEditors = new Set(); + this.isConnected = false; } - connect() { - this.ws = new WebSocket(this.serverUrl); - this.ws.onopen = () => this.initializeLSP(); - this.ws.onmessage = (event) => this.handleMessage(event); - this.ws.onerror = (error) => console.error("WebSocket error:", error); - this.ws.onclose = () => console.log("WebSocket closed"); + /** + * Connect to the LSP server + * @returns {Promise} + */ + async connect() { + if (this.isConnected) { + console.warn('LSP client is already connected'); + return; + } + + try { + // Create WebSocket connection + this.socket = new WebSocket(this.wsUrl); + + // Set up WebSocket event handlers + this.socket.onopen = () => { + console.log('LSP WebSocket connected'); + this.isConnected = true; + this.reconnectAttempts = 0; + this._initializeLanguageProvider(); + }; + + this.socket.onclose = (event) => { + console.log('LSP WebSocket disconnected', event); + this.isConnected = false; + }; + + this.socket.onerror = (error) => { + console.error('LSP WebSocket error:', error); + this.isConnected = false; + }; + + // Wait for connection to be established + await this._waitForConnection(); + } catch (error) { + console.error('Failed to connect to LSP server:', error); + throw error; + } } + /** + * Disconnect from the LSP server + */ disconnect() { - this.ws?.close(); - } + if (!this.isConnected) { + console.warn('LSP client is not connected'); + return; + } - initializeLSP() { - const initParams = { - processId: null, - clientInfo: { name: "ace-lsp-client" }, - capabilities: { - textDocument: { - completion: { dynamicRegistration: false }, - publishDiagnostics: { relatedInformation: true }, - }, - }, - }; + try { + // Remove all registered editors + this.registeredEditors.forEach(editor => { + this.removeEditor(editor); + }); + + // Clean up language provider + if (this.languageProvider) { + this.languageProvider.dispose?.(); + this.languageProvider = null; + } - this.sendRequest("initialize", initParams) - .then(() => { - this.sendNotification("initialized", {}); - // Open all registered editors - for (const [id] of this.editors) { - this.sendDidOpen(id); - } - }) - .catch((error) => console.error("Initialization failed:", error)); - } + // Close WebSocket connection + if (this.socket) { + this.socket.close(); + this.socket = null; + } - addEditor(id, editor, documentUri, language, mode) { - if (this.editors.has(id)) { - this.removeEditor(id); + this.isConnected = false; + console.log('LSP client disconnected'); + } catch (error) { + console.error('Error during disconnect:', error); } + } - // Setup ace-linters with mode - const session = editor.getSession(); + /** + * Add an editor to the language provider + * @param {Object} editor - Ace editor instance + * @returns {boolean} - Success status + */ + addEditor(editor) { + if (!editor) { + console.error('Editor is required'); + return false; + } - session.setMode(mode); - session.setUseWorker(true); + if (this.registeredEditors.has(editor)) { + console.warn('Editor is already registered'); + return true; + } - // Enable autocompletion - editor.setOptions({ - enableBasicAutocompletion: true, - enableLiveAutocompletion: true, - enableSnippets: false, - }); + if (!this.isConnected || !this.languageProvider) { + console.error('LSP client is not connected. Call connect() first.'); + return false; + } - // Add LSP completer - editor.completers = editor.completers || []; - editor.completers.push({ - getCompletions: (ed, session, pos, prefix, callback) => { - this.requestCompletions(id, pos, callback); - }, - }); + try { - // Track changes for LSP - const changeHandler = () => this.sendDidChange(id); - session.on("change", changeHandler); + this.languageProvider.registerEditor(editor); + this.registeredEditors.add(editor); + + const session = editor.getSession(); + session.on('changeAnnotation', () => { + editor.renderer.updateBackMarkers(); + }); + console.log('Editor registered with LSP client'); + return true; + } catch (error) { + console.error('Failed to register editor:', error); + return false; + } + } - this.editors.set(id, { - editor, - documentUri, - language, - version: 1, - changeHandler, - }); + /** + * Remove an editor from the language provider + * @param {Object} editor - Ace editor instance + * @returns {boolean} - Success status + */ + removeEditor(editor) { + if (!editor) { + console.error('Editor is required'); + return false; + } - this.sendDidOpen(id); - } + if (!this.registeredEditors.has(editor)) { + console.warn('Editor is not registered'); + return true; + } - removeEditor(id) { - const meta = this.editors.get(id); - if (!meta) return; + try { + // Unregister from language provider if available + if (this.languageProvider && this.languageProvider.unregisterEditor) { + this.languageProvider.unregisterEditor(editor); + } - // Clean up - meta.editor.getSession().removeListener("change", meta.changeHandler); - this.sendNotification("textDocument/didClose", { - textDocument: { uri: meta.documentUri }, - }); - this.editors.delete(id); + this.registeredEditors.delete(editor); + console.log('Editor unregistered from LSP client'); + return true; + } catch (error) { + console.error('Failed to unregister editor:', error); + return false; + } } - sendDidOpen(id) { - const meta = this.editors.get(id); - if (!meta) return; - - this.sendNotification("textDocument/didOpen", { - textDocument: { - uri: meta.documentUri, - languageId: meta.language, - version: meta.version, - text: meta.editor.getValue(), - }, - }); + /** + * Get the current connection status + * @returns {boolean} + */ + isConnectedToServer() { + return this.isConnected && this.socket && this.socket.readyState === WebSocket.OPEN; } - sendDidChange(id) { - const meta = this.editors.get(id); - if (!meta) return; - - meta.version += 1; - this.sendNotification("textDocument/didChange", { - textDocument: { - uri: meta.documentUri, - version: meta.version, - }, - contentChanges: [{ text: meta.editor.getValue() }], - }); + /** + * Get the list of registered editors + * @returns {Set} + */ + getRegisteredEditors() { + return new Set(this.registeredEditors); } - requestCompletions(id, position, callback) { - const meta = this.editors.get(id); - if (!meta) { - callback(null, []); - return; - } - - const params = { - textDocument: { uri: meta.documentUri }, - position: { line: position.row, character: position.column }, + // Private helper methods + + /** + * Initialize the language provider after connection is established + * @private + */ + _initializeLanguageProvider() { + const serverData = { + module: () => import("ace-linters/build/language-client"), + modes: this.modes, + type: "socket", + socket: this.socket, }; - this.sendRequest("textDocument/completion", params) - .then((result) => { - const completions = (result?.items || []).map((item) => ({ - caption: item.label, - value: item.insertText || item.label, - meta: item.detail || "lsp", - })); - callback(null, completions); - }) - .catch(() => callback(null, [])); + try { + this.languageProvider = AceLanguageClient.for(serverData); + console.log('Language provider initialized'); + } catch (error) { + console.error('Failed to initialize language provider:', error); + throw error; + } } - sendRequest(method, params) { + /** + * Wait for WebSocket connection to be established + * @private + * @returns {Promise} + */ + _waitForConnection() { return new Promise((resolve, reject) => { - if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { - reject(new Error("WebSocket not open")); - return; + const timeout = setTimeout(() => { + reject(new Error('Connection timeout')); + }, 10000); // 10 second timeout + + if (this.socket.readyState === WebSocket.OPEN) { + clearTimeout(timeout); + resolve(); + } else { + this.socket.addEventListener('open', () => { + clearTimeout(timeout); + resolve(); + }); + + this.socket.addEventListener('error', (error) => { + clearTimeout(timeout); + reject(error); + }); } - - const id = ++this.messageId; - const message = { jsonrpc: "2.0", id, method, params }; - this.pendingRequests.set(id, { resolve, reject }); - this.ws.send(JSON.stringify(message)); }); } +} - sendNotification(method, params) { - if (this.ws?.readyState === WebSocket.OPEN) { - const message = { jsonrpc: "2.0", method, params }; - this.ws.send(JSON.stringify(message)); - } - } +// Usage example: +/* +const client = new lspClient('ws://localhost:8080/lsp', ['javascript', 'typescript']); - handleMessage(event) { - let message; - try { - message = JSON.parse(event.data); - } catch (e) { - console.warn("Failed to parse LSP message", e); - return; - } +// Connect to server +await client.connect(); - // Handle responses - if (message.id && this.pendingRequests.has(message.id)) { - const { resolve, reject } = this.pendingRequests.get(message.id); - message.error ? reject(message.error) : resolve(message.result); - this.pendingRequests.delete(message.id); - } - // Handle diagnostics - add to ace-linters annotations - else if (message.method === "textDocument/publishDiagnostics") { - this.handleDiagnostics(message.params); - } - } +// Add editors +const editor1 = editorManager.activeFile.session.$editor; +client.addEditor(editor1); - handleDiagnostics(params) { - const diagnostics = params.diagnostics || []; - const uri = params.uri; - if (!uri) return; - - // Find editors with matching URI - for (const [, meta] of this.editors) { - if (meta.documentUri === uri) { - const session = meta.editor.getSession(); - - // Get existing ace-linters annotations - const existing = session.getAnnotations() || []; - const linterAnnotations = existing.filter((a) => a.source !== "lsp"); - - // Add LSP diagnostics - const lspAnnotations = diagnostics.map((d) => ({ - row: d.range.start.line, - column: d.range.start.character, - text: d.message, - type: d.severity === 1 ? "error" : "warning", - source: "lsp", - })); - - // Combine and set - session.setAnnotations([...linterAnnotations, ...lspAnnotations]); - } - } - } -} +// Later, remove editor +client.removeEditor(editor1); + +// Disconnect when done +client.disconnect(); +*/ \ No newline at end of file From e7b268299743c3f25c3dc6e443f3a035b99e3e32 Mon Sep 17 00:00:00 2001 From: RohitKushvaha01 Date: Tue, 5 Aug 2025 16:43:41 +0530 Subject: [PATCH 09/18] format --- src/lib/lspClient.js | 59 +++++++++++++++++++++++--------------------- 1 file changed, 31 insertions(+), 28 deletions(-) diff --git a/src/lib/lspClient.js b/src/lib/lspClient.js index 6089c64d4..4261ed7c7 100644 --- a/src/lib/lspClient.js +++ b/src/lib/lspClient.js @@ -4,7 +4,7 @@ export default class lspClient { constructor(wsUrl, modes) { this.wsUrl = wsUrl; // Convert modes array to string if needed, or keep as string - this.modes = Array.isArray(modes) ? modes.join(',') : modes; + this.modes = Array.isArray(modes) ? modes.join(",") : modes; this.socket = null; this.languageProvider = null; this.registeredEditors = new Set(); @@ -17,7 +17,7 @@ export default class lspClient { */ async connect() { if (this.isConnected) { - console.warn('LSP client is already connected'); + console.warn("LSP client is already connected"); return; } @@ -27,26 +27,26 @@ export default class lspClient { // Set up WebSocket event handlers this.socket.onopen = () => { - console.log('LSP WebSocket connected'); + console.log("LSP WebSocket connected"); this.isConnected = true; this.reconnectAttempts = 0; this._initializeLanguageProvider(); }; this.socket.onclose = (event) => { - console.log('LSP WebSocket disconnected', event); + console.log("LSP WebSocket disconnected", event); this.isConnected = false; }; this.socket.onerror = (error) => { - console.error('LSP WebSocket error:', error); + console.error("LSP WebSocket error:", error); this.isConnected = false; }; // Wait for connection to be established await this._waitForConnection(); } catch (error) { - console.error('Failed to connect to LSP server:', error); + console.error("Failed to connect to LSP server:", error); throw error; } } @@ -56,13 +56,13 @@ export default class lspClient { */ disconnect() { if (!this.isConnected) { - console.warn('LSP client is not connected'); + console.warn("LSP client is not connected"); return; } try { // Remove all registered editors - this.registeredEditors.forEach(editor => { + this.registeredEditors.forEach((editor) => { this.removeEditor(editor); }); @@ -79,9 +79,9 @@ export default class lspClient { } this.isConnected = false; - console.log('LSP client disconnected'); + console.log("LSP client disconnected"); } catch (error) { - console.error('Error during disconnect:', error); + console.error("Error during disconnect:", error); } } @@ -92,33 +92,32 @@ export default class lspClient { */ addEditor(editor) { if (!editor) { - console.error('Editor is required'); + console.error("Editor is required"); return false; } if (this.registeredEditors.has(editor)) { - console.warn('Editor is already registered'); + console.warn("Editor is already registered"); return true; } if (!this.isConnected || !this.languageProvider) { - console.error('LSP client is not connected. Call connect() first.'); + console.error("LSP client is not connected. Call connect() first."); return false; } try { - this.languageProvider.registerEditor(editor); this.registeredEditors.add(editor); const session = editor.getSession(); - session.on('changeAnnotation', () => { + session.on("changeAnnotation", () => { editor.renderer.updateBackMarkers(); }); - console.log('Editor registered with LSP client'); + console.log("Editor registered with LSP client"); return true; } catch (error) { - console.error('Failed to register editor:', error); + console.error("Failed to register editor:", error); return false; } } @@ -130,12 +129,12 @@ export default class lspClient { */ removeEditor(editor) { if (!editor) { - console.error('Editor is required'); + console.error("Editor is required"); return false; } if (!this.registeredEditors.has(editor)) { - console.warn('Editor is not registered'); + console.warn("Editor is not registered"); return true; } @@ -146,10 +145,10 @@ export default class lspClient { } this.registeredEditors.delete(editor); - console.log('Editor unregistered from LSP client'); + console.log("Editor unregistered from LSP client"); return true; } catch (error) { - console.error('Failed to unregister editor:', error); + console.error("Failed to unregister editor:", error); return false; } } @@ -159,7 +158,11 @@ export default class lspClient { * @returns {boolean} */ isConnectedToServer() { - return this.isConnected && this.socket && this.socket.readyState === WebSocket.OPEN; + return ( + this.isConnected && + this.socket && + this.socket.readyState === WebSocket.OPEN + ); } /** @@ -186,9 +189,9 @@ export default class lspClient { try { this.languageProvider = AceLanguageClient.for(serverData); - console.log('Language provider initialized'); + console.log("Language provider initialized"); } catch (error) { - console.error('Failed to initialize language provider:', error); + console.error("Failed to initialize language provider:", error); throw error; } } @@ -201,19 +204,19 @@ export default class lspClient { _waitForConnection() { return new Promise((resolve, reject) => { const timeout = setTimeout(() => { - reject(new Error('Connection timeout')); + reject(new Error("Connection timeout")); }, 10000); // 10 second timeout if (this.socket.readyState === WebSocket.OPEN) { clearTimeout(timeout); resolve(); } else { - this.socket.addEventListener('open', () => { + this.socket.addEventListener("open", () => { clearTimeout(timeout); resolve(); }); - this.socket.addEventListener('error', (error) => { + this.socket.addEventListener("error", (error) => { clearTimeout(timeout); reject(error); }); @@ -238,4 +241,4 @@ client.removeEditor(editor1); // Disconnect when done client.disconnect(); -*/ \ No newline at end of file +*/ From da9744112a8158415bc3d454b3df41ac9cc04109 Mon Sep 17 00:00:00 2001 From: Raunak Raj <71929976+bajrangCoder@users.noreply.github.com> Date: Fri, 8 Aug 2025 11:32:38 +0530 Subject: [PATCH 10/18] show gutter icons --- src/lib/lspClient.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/lib/lspClient.js b/src/lib/lspClient.js index 4261ed7c7..1648040ca 100644 --- a/src/lib/lspClient.js +++ b/src/lib/lspClient.js @@ -1,4 +1,5 @@ import { AceLanguageClient } from "ace-linters/build/ace-language-client"; +import settings from "./settings"; export default class lspClient { constructor(wsUrl, modes) { @@ -107,6 +108,9 @@ export default class lspClient { } try { + settings.update({ + showAnnotations: true, + }); this.languageProvider.registerEditor(editor); this.registeredEditors.add(editor); From 5f93b60501b738e41dff59b083f3ebf317d32f9f Mon Sep 17 00:00:00 2001 From: Raunak Raj <71929976+bajrangCoder@users.noreply.github.com> Date: Fri, 8 Aug 2025 12:01:39 +0530 Subject: [PATCH 11/18] improve styling of ace tooltip and doc to match app theme --- src/styles/overrideAceStyle.scss | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/src/styles/overrideAceStyle.scss b/src/styles/overrideAceStyle.scss index 7fb00cbeb..af6e21e0c 100644 --- a/src/styles/overrideAceStyle.scss +++ b/src/styles/overrideAceStyle.scss @@ -18,12 +18,24 @@ } .ace_tooltip { - background-color: rgb(255, 255, 255); - background-color: var(--secondary-color); - color: rgb(37, 37, 37); - color: var(--secondary-text-color); - max-width: 68%; + background-color: var(--popup-background-color) !important; + color: var(--popup-text-color) !important; + border: 1px solid var(--popup-border-color) !important; + border-radius: var(--popup-border-radius) !important; + box-shadow: 0 4px 12px var(--box-shadow-color) !important; + padding: 8px 12px; + font-size: 13px; + line-height: 1.4; + max-width: 400px; + min-width: 120px; white-space: pre-wrap; + word-wrap: break-word; + transition: opacity 0.2s ease-in-out; + + &.ace_doc-tooltip { + background-color: var(--popup-background-color) !important; + border-color: var(--border-color) !important; + } } main .ace_editor { From 834728bb230e5418c0782c92c3d73b0318ce0d94 Mon Sep 17 00:00:00 2001 From: Raunak Raj <71929976+bajrangCoder@users.noreply.github.com> Date: Fri, 8 Aug 2025 12:36:44 +0530 Subject: [PATCH 12/18] remove useless styles --- src/styles/overrideAceStyle.scss | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/styles/overrideAceStyle.scss b/src/styles/overrideAceStyle.scss index af6e21e0c..231a0b551 100644 --- a/src/styles/overrideAceStyle.scss +++ b/src/styles/overrideAceStyle.scss @@ -31,11 +31,6 @@ white-space: pre-wrap; word-wrap: break-word; transition: opacity 0.2s ease-in-out; - - &.ace_doc-tooltip { - background-color: var(--popup-background-color) !important; - border-color: var(--border-color) !important; - } } main .ace_editor { From 8f441117d1d6cc4ce64c9301bb4e6810f8f8ebc6 Mon Sep 17 00:00:00 2001 From: RohitKushvaha01 Date: Wed, 13 Aug 2025 16:22:52 +0530 Subject: [PATCH 13/18] feat: api for setting workspace folder --- src/lib/lspClient.js | 47 +++++++++++++++++++++++++++----------------- www/index.html | 10 +++++----- 2 files changed, 34 insertions(+), 23 deletions(-) diff --git a/src/lib/lspClient.js b/src/lib/lspClient.js index 1648040ca..93214e809 100644 --- a/src/lib/lspClient.js +++ b/src/lib/lspClient.js @@ -157,6 +157,35 @@ export default class lspClient { } } + /** + * + * This method sends a request to the LSP server to change the current workspace folder. + * The server must have access to the specified `uri`. + * + * ⚠️ Note: + * - The LSP server must be running and connected before calling this method. + * - Android SAF (`content://`) URIs are not supported — use a file system–accessible path instead. + * + * @async + * @param {string} uri - The URI of the workspace folder to set (e.g., "file:///path/to/folder"). + * @returns {Promise} Resolves to `true` if the operation was successful, otherwise `false`. + */ + async setWorkspaceFolder(uri) { + if (!this.languageProvider) { + console.error( + "Language provider not initialized. Connect to the LSP server first.", + ); + return false; + } + + try { + await this.languageProvider.changeWorkspaceFolder(uri); + } catch (error) { + console.error("Error setting workspace folder:", error); + return false; + } + } + /** * Get the current connection status * @returns {boolean} @@ -228,21 +257,3 @@ export default class lspClient { }); } } - -// Usage example: -/* -const client = new lspClient('ws://localhost:8080/lsp', ['javascript', 'typescript']); - -// Connect to server -await client.connect(); - -// Add editors -const editor1 = editorManager.activeFile.session.$editor; -client.addEditor(editor1); - -// Later, remove editor -client.removeEditor(editor1); - -// Disconnect when done -client.disconnect(); -*/ diff --git a/www/index.html b/www/index.html index 6ceca12d1..ae9aa7f4d 100644 --- a/www/index.html +++ b/www/index.html @@ -165,17 +165,17 @@ Acode - - - - - + + + + + From bf1d54d1aa13e4dc67e90ce34f09252d2dca4c22 Mon Sep 17 00:00:00 2001 From: RohitKushvaha01 Date: Thu, 14 Aug 2025 09:20:54 +0530 Subject: [PATCH 14/18] feat: format document --- src/lib/lspClient.js | 88 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/src/lib/lspClient.js b/src/lib/lspClient.js index 93214e809..0584ce57d 100644 --- a/src/lib/lspClient.js +++ b/src/lib/lspClient.js @@ -186,6 +186,94 @@ export default class lspClient { } } + /** + * Format the current document or selection + * @param {Object} editor - Ace editor instance + * @param {Object} [range] - Formatting range (optional) + * @returns {Promise} + */ + async formatDocument(editor, range) { + if (!this.isConnected || !this.languageProvider) return false; + + try { + await this.languageProvider.format(editor, range); + return true; + } catch (error) { + console.error("Formatting failed:", error); + return false; + } + } + + /** + * Get hover information at current cursor position + * @param {Object} editor - Ace editor instance + * @returns {Promise} + */ + async getHoverInfo(editor) { + if (!this.isConnected || !this.languageProvider) return null; + + const session = editor.getSession(); + const cursor = editor.getCursorPosition(); + + try { + return await this.languageProvider.doHover(session, cursor); + } catch (error) { + console.error("Hover request failed:", error); + return null; + } + } + + /** + * Go to definition of symbol at cursor + * @param {Object} editor - Ace editor instance + * @returns {Promise} Location of definition + */ + async goToDefinition(editor, uri) { + if (!this.isConnected || !this.languageProvider?.client) return null; + + const session = editor.getSession(); + const cursor = editor.getCursorPosition(); + + try { + return await this.languageProvider.sendRequest( + "textDocument/definition", + { + textDocument: { uri }, + position: { line: cursor.row, character: cursor.column }, + }, + ); + } catch (error) { + console.error("Go to definition failed:", error); + return null; + } + } + + /** + * Find references to symbol at cursor + * @param {Object} editor - Ace editor instance + * @returns {Promise} Reference locations + */ + async findReferences(editor, uri) { + if (!this.isConnected || !this.languageProvider?.client) return null; + + const session = editor.getSession(); + const cursor = editor.getCursorPosition(); + + try { + return await this.languageProvider.sendRequest( + "textDocument/references", + { + textDocument: { uri }, + position: { line: cursor.row, character: cursor.column }, + context: { includeDeclaration: true }, + }, + ); + } catch (error) { + console.error("Find references failed:", error); + return null; + } + } + /** * Get the current connection status * @returns {boolean} From 2e54ec648052d22f30a1b567d202721e3f95391f Mon Sep 17 00:00:00 2001 From: RohitKushvaha01 Date: Sat, 16 Aug 2025 13:05:07 +0530 Subject: [PATCH 15/18] feat: add initializationOptions --- src/lib/lspClient.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/lib/lspClient.js b/src/lib/lspClient.js index 0584ce57d..cb04336a2 100644 --- a/src/lib/lspClient.js +++ b/src/lib/lspClient.js @@ -2,13 +2,14 @@ import { AceLanguageClient } from "ace-linters/build/ace-language-client"; import settings from "./settings"; export default class lspClient { - constructor(wsUrl, modes) { + constructor(wsUrl, modes, initializationOptions = {}) { this.wsUrl = wsUrl; // Convert modes array to string if needed, or keep as string this.modes = Array.isArray(modes) ? modes.join(",") : modes; this.socket = null; this.languageProvider = null; this.registeredEditors = new Set(); + this.initializationOptions = initializationOptions; this.isConnected = false; } @@ -306,10 +307,11 @@ export default class lspClient { modes: this.modes, type: "socket", socket: this.socket, + initializationOptions: this.initializationOptions }; try { - this.languageProvider = AceLanguageClient.for(serverData); + this.languageProvider = AceLanguageClient.for(serverData, this.initializationOptions); console.log("Language provider initialized"); } catch (error) { console.error("Failed to initialize language provider:", error); From cd02f1e2898558913770f720fbb1dbade81a47e0 Mon Sep 17 00:00:00 2001 From: RohitKushvaha01 Date: Sat, 16 Aug 2025 13:08:02 +0530 Subject: [PATCH 16/18] format --- src/lib/lspClient.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/lib/lspClient.js b/src/lib/lspClient.js index cb04336a2..486d0deb0 100644 --- a/src/lib/lspClient.js +++ b/src/lib/lspClient.js @@ -307,11 +307,14 @@ export default class lspClient { modes: this.modes, type: "socket", socket: this.socket, - initializationOptions: this.initializationOptions + initializationOptions: this.initializationOptions, }; try { - this.languageProvider = AceLanguageClient.for(serverData, this.initializationOptions); + this.languageProvider = AceLanguageClient.for( + serverData, + this.initializationOptions, + ); console.log("Language provider initialized"); } catch (error) { console.error("Failed to initialize language provider:", error); From e6d601976813f28e5cc343ba793f372a9314b82a Mon Sep 17 00:00:00 2001 From: RohitKushvaha01 Date: Sat, 16 Aug 2025 15:51:36 +0530 Subject: [PATCH 17/18] feat: getDefination --- package-lock.json | 23 +++++++++++------------ src/lib/lspClient.js | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 12 deletions(-) diff --git a/package-lock.json b/package-lock.json index 479ba8364..949a0f219 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3331,17 +3331,6 @@ "node": ">= 0.6" } }, - - "node_modules/ace-linters": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/ace-linters/-/ace-linters-1.8.1.tgz", - "integrity": "sha512-gA/go8RjNsSnN1zcgBVQa9Uk81E1bAUSgSC41Wp50axz0L4XipesyPHhL9qImMAg8AnAlFA+i0qJbM/bd2zCWQ==", - "license": "MIT", - "dependencies": { - "vscode-languageserver-protocol": "^3.17.5", - "vscode-languageserver-textdocument": "^1.0.12", - "vscode-languageserver-types": "^3.17.5"}}, - "node_modules/accepts/node_modules/mime-types": { "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", @@ -3352,7 +3341,17 @@ }, "engines": { "node": ">= 0.6" - + } + }, + "node_modules/ace-linters": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/ace-linters/-/ace-linters-1.8.1.tgz", + "integrity": "sha512-gA/go8RjNsSnN1zcgBVQa9Uk81E1bAUSgSC41Wp50axz0L4XipesyPHhL9qImMAg8AnAlFA+i0qJbM/bd2zCWQ==", + "license": "MIT", + "dependencies": { + "vscode-languageserver-protocol": "^3.17.5", + "vscode-languageserver-textdocument": "^1.0.12", + "vscode-languageserver-types": "^3.17.5" } }, "node_modules/acorn": { diff --git a/src/lib/lspClient.js b/src/lib/lspClient.js index 486d0deb0..02616214b 100644 --- a/src/lib/lspClient.js +++ b/src/lib/lspClient.js @@ -91,6 +91,8 @@ export default class lspClient { * Add an editor to the language provider * @param {Object} editor - Ace editor instance * @returns {boolean} - Success status + * + * Note: this is not limited to a single editor tab */ addEditor(editor) { if (!editor) { @@ -295,6 +297,36 @@ export default class lspClient { return new Set(this.registeredEditors); } + async sendRequest(method, params) { + return new Promise((resolve, reject) => { + try { + this.languageProvider.sendRequest( + "lspClient", + method, + params, + (result) => { + resolve(result); + } + ); + } catch (err) { + reject(err); + } + }); + } + + async getDefination(editor, uri) { + const result = await this.sendRequest("textDocument/definition", { + textDocument: { uri }, + position: { + line: editor.getCursorPosition().row, + character: editor.getCursorPosition().column, + }, + }); + + return result; + } + + // Private helper methods /** @@ -307,6 +339,7 @@ export default class lspClient { modes: this.modes, type: "socket", socket: this.socket, + serviceName: "lspClient", initializationOptions: this.initializationOptions, }; From 207629854c02e3897ca62485900e4cf1571b8379 Mon Sep 17 00:00:00 2001 From: RohitKushvaha01 Date: Sat, 16 Aug 2025 15:53:15 +0530 Subject: [PATCH 18/18] format --- src/lib/lspClient.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/lib/lspClient.js b/src/lib/lspClient.js index 02616214b..c1ac42dd3 100644 --- a/src/lib/lspClient.js +++ b/src/lib/lspClient.js @@ -91,7 +91,7 @@ export default class lspClient { * Add an editor to the language provider * @param {Object} editor - Ace editor instance * @returns {boolean} - Success status - * + * * Note: this is not limited to a single editor tab */ addEditor(editor) { @@ -306,7 +306,7 @@ export default class lspClient { params, (result) => { resolve(result); - } + }, ); } catch (err) { reject(err); @@ -314,7 +314,7 @@ export default class lspClient { }); } - async getDefination(editor, uri) { + async getDefinition(editor, uri) { const result = await this.sendRequest("textDocument/definition", { textDocument: { uri }, position: { @@ -326,7 +326,6 @@ export default class lspClient { return result; } - // Private helper methods /**