Skip to content

Commit 73900e0

Browse files
feat. allow multiple editors on a single connection
1 parent d1aeeac commit 73900e0

File tree

1 file changed

+136
-72
lines changed

1 file changed

+136
-72
lines changed

src/lib/lspClient.js

Lines changed: 136 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
11
export default class lspClient {
2-
constructor({ serverUrl, editor, documentUri, language }) {
3-
this.editor = editor;
4-
this.documentUri = documentUri;
2+
constructor({ serverUrl }) {
53
this.serverUrl = serverUrl;
64
this.ws = null;
75
this.messageId = 0;
86
this.pendingRequests = new Map();
9-
this.documentVersion = 1;
10-
this.currentLanguage = language;
11-
}
127

8+
// Map of editorId -> { editor, documentUri, language, version, changeHandler }
9+
this.editors = new Map();
10+
}
1311

1412
// Establish WebSocket connection and initialize LSP
1513
connect() {
@@ -20,29 +18,14 @@ export default class lspClient {
2018
this.ws.onmessage = (event) => this.handleMessage(event);
2119
this.ws.onerror = (error) => console.error('WebSocket error:', error);
2220
this.ws.onclose = () => console.log('WebSocket closed');
23-
24-
// Listen to editor changes
25-
this.editor.getSession().on('change', (delta) => {
26-
this.sendDidChange();
27-
});
28-
29-
// Add LSP completer for autocompletion
30-
this.editor.completers = this.editor.completers || [];
31-
this.editor.completers.push({
32-
getCompletions: (editor, session, pos, prefix, callback) => {
33-
this.requestCompletions(pos, prefix, callback);
34-
}
35-
});
3621
}
3722

38-
// Disconnect from the LSP server
3923
disconnect() {
4024
if (this.ws) {
4125
this.ws.close();
4226
}
4327
}
4428

45-
// Send initialize request to LSP server
4629
initializeLSP() {
4730
const initParams = {
4831
processId: null,
@@ -54,75 +37,157 @@ export default class lspClient {
5437
}
5538
}
5639
};
57-
this.sendRequest('initialize', initParams).then((result) => {
58-
this.sendNotification('initialized', {});
59-
this.sendDidOpen();
60-
}).catch((error) => console.error('Initialization failed:', error));
40+
this.sendRequest('initialize', initParams)
41+
.then(() => {
42+
this.sendNotification('initialized', {});
43+
// Open all already-registered editors
44+
for (const [id, meta] of this.editors) {
45+
this.sendDidOpen(id);
46+
}
47+
})
48+
.catch((error) => console.error('Initialization failed:', error));
49+
}
50+
51+
// Add an editor/tab to share this single connection
52+
addEditor(id, editor, documentUri, language) {
53+
if (this.editors.has(id)) {
54+
console.warn(`Editor with id ${id} already registered; replacing.`);
55+
this.removeEditor(id);
56+
}
57+
58+
const meta = {
59+
editor,
60+
documentUri,
61+
language,
62+
version: 1,
63+
changeHandler: null,
64+
};
65+
66+
// change listener
67+
const changeHandler = () => {
68+
this.sendDidChange(id);
69+
};
70+
meta.changeHandler = changeHandler;
71+
editor.getSession().on('change', changeHandler);
72+
73+
// completer for this editor
74+
editor.completers = editor.completers || [];
75+
editor.completers.push({
76+
getCompletions: (ed, session, pos, prefix, callback) => {
77+
this.requestCompletions(id, pos, prefix, callback);
78+
},
79+
});
80+
81+
this.editors.set(id, meta);
82+
83+
// If already initialized, immediately send didOpen
84+
this.sendDidOpen(id);
85+
}
86+
87+
// Remove an editor/tab
88+
removeEditor(id) {
89+
const meta = this.editors.get(id);
90+
if (!meta) return;
91+
const { editor, changeHandler, documentUri } = meta;
92+
93+
// Optionally notify the server that the document is closed
94+
this.sendNotification('textDocument/didClose', {
95+
textDocument: { uri: documentUri },
96+
});
97+
98+
// Tear down listener
99+
if (changeHandler) {
100+
editor.getSession().removeListener('change', changeHandler);
101+
}
102+
103+
// Note: removing completer is left to caller if needed
104+
this.editors.delete(id);
61105
}
62106

63-
// Send textDocument/didOpen notification
64-
sendDidOpen() {
107+
sendDidOpen(id) {
108+
const meta = this.editors.get(id);
109+
if (!meta) return;
110+
const { editor, documentUri, language, version } = meta;
65111
const params = {
66112
textDocument: {
67-
uri: this.documentUri,
68-
languageId: this.currentLanguage,
69-
version: this.documentVersion,
70-
text: this.editor.getValue()
71-
}
113+
uri: documentUri,
114+
languageId: language,
115+
version,
116+
text: editor.getValue(),
117+
},
72118
};
73119
this.sendNotification('textDocument/didOpen', params);
74120
}
75121

76-
// Send textDocument/didChange notification
77-
sendDidChange() {
122+
sendDidChange(id) {
123+
const meta = this.editors.get(id);
124+
if (!meta) return;
125+
const { editor, documentUri } = meta;
126+
meta.version += 1;
78127
const params = {
79128
textDocument: {
80-
uri: this.documentUri,
81-
version: ++this.documentVersion
129+
uri: documentUri,
130+
version: meta.version,
82131
},
83-
contentChanges: [{ text: this.editor.getValue() }]
132+
contentChanges: [{ text: editor.getValue() }],
84133
};
85134
this.sendNotification('textDocument/didChange', params);
86135
}
87136

88-
// Request completions from LSP server
89-
requestCompletions(position, prefix, callback) {
137+
requestCompletions(id, position, prefix, callback) {
138+
const meta = this.editors.get(id);
139+
if (!meta) {
140+
callback(null, []);
141+
return;
142+
}
143+
const { documentUri } = meta;
90144
const params = {
91-
textDocument: { uri: this.documentUri },
92-
position: { line: position.row, character: position.column }
145+
textDocument: { uri: documentUri },
146+
position: { line: position.row, character: position.column },
93147
};
94-
this.sendRequest('textDocument/completion', params).then((result) => {
95-
const completions = (result?.items || []).map(item => ({
96-
caption: item.label,
97-
value: item.insertText || item.label,
98-
meta: item.detail || 'completion'
99-
}));
100-
callback(null, completions);
101-
}).catch((error) => {
102-
console.error('Completion failed:', error);
103-
callback(null, []);
104-
});
148+
this.sendRequest('textDocument/completion', params)
149+
.then((result) => {
150+
const completions = (result?.items || []).map((item) => ({
151+
caption: item.label,
152+
value: item.insertText || item.label,
153+
meta: item.detail || 'completion',
154+
}));
155+
callback(null, completions);
156+
})
157+
.catch((error) => {
158+
console.error('Completion failed:', error);
159+
callback(null, []);
160+
});
105161
}
106162

107-
// Send a request and return a promise for the response
108163
sendRequest(method, params) {
109164
return new Promise((resolve, reject) => {
165+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
166+
reject(new Error('WebSocket not open'));
167+
return;
168+
}
110169
const id = ++this.messageId;
111170
const message = { jsonrpc: '2.0', id, method, params };
112171
this.pendingRequests.set(id, { resolve, reject });
113172
this.ws.send(JSON.stringify(message));
114173
});
115174
}
116175

117-
// Send a notification (no response expected)
118176
sendNotification(method, params) {
177+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
119178
const message = { jsonrpc: '2.0', method, params };
120179
this.ws.send(JSON.stringify(message));
121180
}
122181

123-
// Handle incoming WebSocket messages
124182
handleMessage(event) {
125-
const message = JSON.parse(event.data);
183+
let message;
184+
try {
185+
message = JSON.parse(event.data);
186+
} catch (e) {
187+
console.warn('Failed to parse LSP message', e);
188+
return;
189+
}
190+
126191
if (message.id && this.pendingRequests.has(message.id)) {
127192
const { resolve, reject } = this.pendingRequests.get(message.id);
128193
if (message.error) {
@@ -136,23 +201,22 @@ export default class lspClient {
136201
}
137202
}
138203

139-
// Handle diagnostics from LSP server and display in editor
140204
handleDiagnostics(params) {
141205
const diagnostics = params.diagnostics || [];
142-
const annotations = diagnostics.map(d => ({
143-
row: d.range.start.line,
144-
column: d.range.start.character,
145-
text: d.message,
146-
type: d.severity === 1 ? 'error' : 'warning'
147-
}));
148-
this.editor.getSession().setAnnotations(annotations);
149-
150-
// Optional: Update diagnostics list in HTML (assumes element exists)
151-
const diagnosticsList = document.getElementById('diagnosticsList');
152-
if (diagnosticsList) {
153-
diagnosticsList.innerHTML = diagnostics.map(d =>
154-
`<li>${d.message} at line ${d.range.start.line + 1}</li>`
155-
).join('');
206+
const uri = params.uri || (params.textDocument && params.textDocument.uri);
207+
if (!uri) return;
208+
209+
// Find all editors with that document URI
210+
for (const [, meta] of this.editors) {
211+
if (meta.documentUri === uri) {
212+
const annotations = diagnostics.map((d) => ({
213+
row: d.range.start.line,
214+
column: d.range.start.character,
215+
text: d.message,
216+
type: d.severity === 1 ? 'error' : 'warning',
217+
}));
218+
meta.editor.getSession().setAnnotations(annotations);
219+
}
156220
}
157221
}
158-
}
222+
}

0 commit comments

Comments
 (0)