1+ import "ace-linters" ;
2+
13export 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