Skip to content

Commit d2fec9f

Browse files
ace-linters
1 parent d5bee49 commit d2fec9f

File tree

3 files changed

+124
-77
lines changed

3 files changed

+124
-77
lines changed

package-lock.json

Lines changed: 43 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@
105105
"@xterm/addon-web-links": "^0.11.0",
106106
"@xterm/addon-webgl": "^0.18.0",
107107
"@xterm/xterm": "^5.5.0",
108+
"ace-linters": "^1.8.1",
108109
"autosize": "^6.0.1",
109110
"cordova": "12.0.0",
110111
"core-js": "^3.37.1",

src/lib/lspClient.js

Lines changed: 80 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,27 @@
1+
import "ace-linters";
2+
13
export default class lspClient {
24
constructor(serverUrl) {
35
this.serverUrl = serverUrl;
46
this.ws = null;
57
this.messageId = 0;
68
this.pendingRequests = new Map();
9+
this.editors = new Map(); // editorId -> { editor, documentUri, language, version }
710

8-
// Map of editorId -> { editor, documentUri, language, version, changeHandler }
9-
this.editors = new Map();
11+
// Enable ace language tools
12+
ace.require("ace/ext/language_tools");
1013
}
1114

12-
// Establish WebSocket connection and initialize LSP
1315
connect() {
1416
this.ws = new WebSocket(this.serverUrl);
15-
this.ws.onopen = () => {
16-
this.initializeLSP();
17-
};
17+
this.ws.onopen = () => this.initializeLSP();
1818
this.ws.onmessage = (event) => this.handleMessage(event);
1919
this.ws.onerror = (error) => console.error("WebSocket error:", error);
2020
this.ws.onclose = () => console.log("WebSocket closed");
2121
}
2222

2323
disconnect() {
24-
if (this.ws) {
25-
this.ws.close();
26-
}
24+
this.ws?.close();
2725
}
2826

2927
initializeLSP() {
@@ -37,127 +35,121 @@ export default class lspClient {
3735
},
3836
},
3937
};
38+
4039
this.sendRequest("initialize", initParams)
4140
.then(() => {
4241
this.sendNotification("initialized", {});
43-
// Open all already-registered editors
44-
for (const [id, meta] of this.editors) {
42+
// Open all registered editors
43+
for (const [id] of this.editors) {
4544
this.sendDidOpen(id);
4645
}
4746
})
4847
.catch((error) => console.error("Initialization failed:", error));
4948
}
5049

51-
// Add an editor/tab to share this single connection
52-
addEditor(id, editor, documentUri, language) {
50+
addEditor(id, editor, documentUri, language, mode) {
5351
if (this.editors.has(id)) {
54-
console.warn(`Editor with id ${id} already registered; replacing.`);
5552
this.removeEditor(id);
5653
}
5754

58-
const meta = {
59-
editor,
60-
documentUri,
61-
language,
62-
version: 1,
63-
changeHandler: null,
64-
};
55+
// Setup ace-linters with mode
56+
const session = editor.getSession();
6557

66-
// change listener
67-
const changeHandler = () => {
68-
this.sendDidChange(id);
69-
};
70-
meta.changeHandler = changeHandler;
71-
editor.getSession().on("change", changeHandler);
58+
session.setMode(mode);
59+
session.setUseWorker(true);
7260

73-
// completer for this editor
61+
// Enable autocompletion
62+
editor.setOptions({
63+
enableBasicAutocompletion: true,
64+
enableLiveAutocompletion: true,
65+
enableSnippets: false,
66+
});
67+
68+
// Add LSP completer
7469
editor.completers = editor.completers || [];
7570
editor.completers.push({
7671
getCompletions: (ed, session, pos, prefix, callback) => {
77-
this.requestCompletions(id, pos, prefix, callback);
72+
this.requestCompletions(id, pos, callback);
7873
},
7974
});
8075

81-
this.editors.set(id, meta);
76+
// Track changes for LSP
77+
const changeHandler = () => this.sendDidChange(id);
78+
session.on("change", changeHandler);
79+
80+
this.editors.set(id, {
81+
editor,
82+
documentUri,
83+
language,
84+
version: 1,
85+
changeHandler,
86+
});
8287

83-
// If already initialized, immediately send didOpen
8488
this.sendDidOpen(id);
8589
}
8690

87-
// Remove an editor/tab
8891
removeEditor(id) {
8992
const meta = this.editors.get(id);
9093
if (!meta) return;
91-
const { editor, changeHandler, documentUri } = meta;
9294

93-
// Optionally notify the server that the document is closed
95+
// Clean up
96+
meta.editor.getSession().removeListener("change", meta.changeHandler);
9497
this.sendNotification("textDocument/didClose", {
95-
textDocument: { uri: documentUri },
98+
textDocument: { uri: meta.documentUri },
9699
});
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
104100
this.editors.delete(id);
105101
}
106102

107103
sendDidOpen(id) {
108104
const meta = this.editors.get(id);
109105
if (!meta) return;
110-
const { editor, documentUri, language, version } = meta;
111-
const params = {
106+
107+
this.sendNotification("textDocument/didOpen", {
112108
textDocument: {
113-
uri: documentUri,
114-
languageId: language,
115-
version,
116-
text: editor.getValue(),
109+
uri: meta.documentUri,
110+
languageId: meta.language,
111+
version: meta.version,
112+
text: meta.editor.getValue(),
117113
},
118-
};
119-
this.sendNotification("textDocument/didOpen", params);
114+
});
120115
}
121116

122117
sendDidChange(id) {
123118
const meta = this.editors.get(id);
124119
if (!meta) return;
125-
const { editor, documentUri } = meta;
120+
126121
meta.version += 1;
127-
const params = {
122+
this.sendNotification("textDocument/didChange", {
128123
textDocument: {
129-
uri: documentUri,
124+
uri: meta.documentUri,
130125
version: meta.version,
131126
},
132-
contentChanges: [{ text: editor.getValue() }],
133-
};
134-
this.sendNotification("textDocument/didChange", params);
127+
contentChanges: [{ text: meta.editor.getValue() }],
128+
});
135129
}
136130

137-
requestCompletions(id, position, prefix, callback) {
131+
requestCompletions(id, position, callback) {
138132
const meta = this.editors.get(id);
139133
if (!meta) {
140134
callback(null, []);
141135
return;
142136
}
143-
const { documentUri } = meta;
137+
144138
const params = {
145-
textDocument: { uri: documentUri },
139+
textDocument: { uri: meta.documentUri },
146140
position: { line: position.row, character: position.column },
147141
};
142+
148143
this.sendRequest("textDocument/completion", params)
149144
.then((result) => {
150145
const completions = (result?.items || []).map((item) => ({
151146
caption: item.label,
152147
value: item.insertText || item.label,
153-
meta: item.detail || "completion",
148+
meta: item.detail || "lsp",
154149
}));
155150
callback(null, completions);
156151
})
157-
.catch((error) => {
158-
console.error("Completion failed:", error);
159-
callback(null, []);
160-
});
152+
.catch(() => callback(null, []));
161153
}
162154

163155
sendRequest(method, params) {
@@ -166,6 +158,7 @@ export default class lspClient {
166158
reject(new Error("WebSocket not open"));
167159
return;
168160
}
161+
169162
const id = ++this.messageId;
170163
const message = { jsonrpc: "2.0", id, method, params };
171164
this.pendingRequests.set(id, { resolve, reject });
@@ -174,9 +167,10 @@ export default class lspClient {
174167
}
175168

176169
sendNotification(method, params) {
177-
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
178-
const message = { jsonrpc: "2.0", method, params };
179-
this.ws.send(JSON.stringify(message));
170+
if (this.ws?.readyState === WebSocket.OPEN) {
171+
const message = { jsonrpc: "2.0", method, params };
172+
this.ws.send(JSON.stringify(message));
173+
}
180174
}
181175

182176
handleMessage(event) {
@@ -188,34 +182,43 @@ export default class lspClient {
188182
return;
189183
}
190184

185+
// Handle responses
191186
if (message.id && this.pendingRequests.has(message.id)) {
192187
const { resolve, reject } = this.pendingRequests.get(message.id);
193-
if (message.error) {
194-
reject(message.error);
195-
} else {
196-
resolve(message.result);
197-
}
188+
message.error ? reject(message.error) : resolve(message.result);
198189
this.pendingRequests.delete(message.id);
199-
} else if (message.method === "textDocument/publishDiagnostics") {
190+
}
191+
// Handle diagnostics - add to ace-linters annotations
192+
else if (message.method === "textDocument/publishDiagnostics") {
200193
this.handleDiagnostics(message.params);
201194
}
202195
}
203196

204197
handleDiagnostics(params) {
205198
const diagnostics = params.diagnostics || [];
206-
const uri = params.uri || (params.textDocument && params.textDocument.uri);
199+
const uri = params.uri;
207200
if (!uri) return;
208201

209-
// Find all editors with that document URI
202+
// Find editors with matching URI
210203
for (const [, meta] of this.editors) {
211204
if (meta.documentUri === uri) {
212-
const annotations = diagnostics.map((d) => ({
205+
const session = meta.editor.getSession();
206+
207+
// Get existing ace-linters annotations
208+
const existing = session.getAnnotations() || [];
209+
const linterAnnotations = existing.filter((a) => a.source !== "lsp");
210+
211+
// Add LSP diagnostics
212+
const lspAnnotations = diagnostics.map((d) => ({
213213
row: d.range.start.line,
214214
column: d.range.start.character,
215215
text: d.message,
216216
type: d.severity === 1 ? "error" : "warning",
217+
source: "lsp",
217218
}));
218-
meta.editor.getSession().setAnnotations(annotations);
219+
220+
// Combine and set
221+
session.setAnnotations([...linterAnnotations, ...lspAnnotations]);
219222
}
220223
}
221224
}

0 commit comments

Comments
 (0)