11export 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