@@ -19,10 +19,12 @@ import * as c from "./constants";
19
19
import * as chokidar from "chokidar" ;
20
20
import { assert } from "console" ;
21
21
import { fileURLToPath } from "url" ;
22
+ import { ChildProcess } from "child_process" ;
22
23
23
24
// https://microsoft.github.io/language-server-protocol/specification#initialize
24
25
// According to the spec, there could be requests before the 'initialize' request. Link in comment tells how to handle them.
25
26
let initialized = false ;
27
+ let serverSentRequestIdCounter = 0 ;
26
28
// https://microsoft.github.io/language-server-protocol/specification#exit
27
29
let shutdownRequestAlreadyReceived = false ;
28
30
let stupidFileContentCache : Map < string , string > = new Map ( ) ;
@@ -31,6 +33,7 @@ let projectsFiles: Map<
31
33
{
32
34
openFiles : Set < string > ;
33
35
filesWithDiagnostics : Set < string > ;
36
+ bsbWatcherByEditor : null | ChildProcess ;
34
37
}
35
38
> = new Map ( ) ;
36
39
// ^ caching AND states AND distributed system. Why does LSP has to be stupid like this
@@ -109,6 +112,10 @@ let stopWatchingCompilerLog = () => {
109
112
compilerLogsWatcher . close ( ) ;
110
113
} ;
111
114
115
+ type clientSentBuildAction = {
116
+ title : string ;
117
+ projectRootPath : string ;
118
+ } ;
112
119
let openedFile = ( fileUri : string , fileContent : string ) => {
113
120
let filePath = fileURLToPath ( fileUri ) ;
114
121
@@ -120,13 +127,46 @@ let openedFile = (fileUri: string, fileContent: string) => {
120
127
projectsFiles . set ( projectRootPath , {
121
128
openFiles : new Set ( ) ,
122
129
filesWithDiagnostics : new Set ( ) ,
130
+ bsbWatcherByEditor : null ,
123
131
} ) ;
124
132
compilerLogsWatcher . add (
125
133
path . join ( projectRootPath , c . compilerLogPartialPath )
126
134
) ;
127
135
}
128
136
let root = projectsFiles . get ( projectRootPath ) ! ;
129
137
root . openFiles . add ( filePath ) ;
138
+ // check if .bsb.lock is still there. If not, start a bsb -w ourselves
139
+ // because otherwise the diagnostics info we'll display might be stale
140
+ let bsbLockPath = path . join ( projectRootPath , c . bsbLock ) ;
141
+ if ( ! fs . existsSync ( bsbLockPath ) ) {
142
+ let bsbPath = path . join ( projectRootPath , c . bsbPartialPath ) ;
143
+ // TODO: sometime stale .bsb.lock dangling
144
+ // TODO: close watcher when lang-server shuts down
145
+ if ( fs . existsSync ( bsbPath ) ) {
146
+ let payload : clientSentBuildAction = {
147
+ title : c . startBuildAction ,
148
+ projectRootPath : projectRootPath ,
149
+ } ;
150
+ let params = {
151
+ type : p . MessageType . Info ,
152
+ message : `Start a build for this project to get the freshest data?` ,
153
+ actions : [ payload ] ,
154
+ } ;
155
+ let request : m . RequestMessage = {
156
+ jsonrpc : c . jsonrpcVersion ,
157
+ id : serverSentRequestIdCounter ++ ,
158
+ method : "window/showMessageRequest" ,
159
+ params : params ,
160
+ } ;
161
+ process . send ! ( request ) ;
162
+ // the client might send us back the "start build" action, which we'll
163
+ // handle in the isResponseMessage check in the message handling way
164
+ // below
165
+ } else {
166
+ // we should send something to say that we can't find bsb.exe. But right now we'll silently not do anything
167
+ }
168
+ }
169
+
130
170
// no need to call sendUpdatedDiagnostics() here; the watcher add will
131
171
// call the listener which calls it
132
172
}
@@ -147,6 +187,10 @@ let closedFile = (fileUri: string) => {
147
187
path . join ( projectRootPath , c . compilerLogPartialPath )
148
188
) ;
149
189
deleteProjectDiagnostics ( projectRootPath ) ;
190
+ if ( root . bsbWatcherByEditor !== null ) {
191
+ root . bsbWatcherByEditor . kill ( ) ;
192
+ root . bsbWatcherByEditor = null ;
193
+ }
150
194
}
151
195
}
152
196
}
@@ -163,29 +207,28 @@ let getOpenedFileContent = (fileUri: string) => {
163
207
return content ;
164
208
} ;
165
209
166
- process . on ( "message" , ( a : m . RequestMessage | m . NotificationMessage ) => {
167
- if ( ( a as m . RequestMessage ) . id == null ) {
168
- // this is a notification message, aka the client ends it and doesn't want a reply
169
- let aa = a as m . NotificationMessage ;
170
- if ( ! initialized && aa . method !== "exit" ) {
210
+ process . on ( "message" , ( msg : m . Message ) => {
211
+ if ( m . isNotificationMessage ( msg ) ) {
212
+ // notification message, aka the client ends it and doesn't want a reply
213
+ if ( ! initialized && msg . method !== "exit" ) {
171
214
// From spec: "Notifications should be dropped, except for the exit notification. This will allow the exit of a server without an initialize request"
172
215
// For us: do nothing. We don't have anything we need to clean up right now
173
216
// TODO: we might have things we need to clean up now... like some watcher stuff
174
- } else if ( aa . method === "exit" ) {
217
+ } else if ( msg . method === "exit" ) {
175
218
// The server should exit with success code 0 if the shutdown request has been received before; otherwise with error code 1
176
219
if ( shutdownRequestAlreadyReceived ) {
177
220
process . exit ( 0 ) ;
178
221
} else {
179
222
process . exit ( 1 ) ;
180
223
}
181
- } else if ( aa . method === DidOpenTextDocumentNotification . method ) {
182
- let params = aa . params as p . DidOpenTextDocumentParams ;
224
+ } else if ( msg . method === DidOpenTextDocumentNotification . method ) {
225
+ let params = msg . params as p . DidOpenTextDocumentParams ;
183
226
let extName = path . extname ( params . textDocument . uri ) ;
184
227
if ( extName === c . resExt || extName === c . resiExt ) {
185
228
openedFile ( params . textDocument . uri , params . textDocument . text ) ;
186
229
}
187
- } else if ( aa . method === DidChangeTextDocumentNotification . method ) {
188
- let params = aa . params as p . DidChangeTextDocumentParams ;
230
+ } else if ( msg . method === DidChangeTextDocumentNotification . method ) {
231
+ let params = msg . params as p . DidChangeTextDocumentParams ;
189
232
let extName = path . extname ( params . textDocument . uri ) ;
190
233
if ( extName === c . resExt || extName === c . resiExt ) {
191
234
let changes = params . contentChanges ;
@@ -199,24 +242,23 @@ process.on("message", (a: m.RequestMessage | m.NotificationMessage) => {
199
242
) ;
200
243
}
201
244
}
202
- } else if ( aa . method === DidCloseTextDocumentNotification . method ) {
203
- let params = aa . params as p . DidCloseTextDocumentParams ;
245
+ } else if ( msg . method === DidCloseTextDocumentNotification . method ) {
246
+ let params = msg . params as p . DidCloseTextDocumentParams ;
204
247
closedFile ( params . textDocument . uri ) ;
205
248
}
206
- } else {
207
- // this is a request message, aka client sent request and waits for our mandatory reply
208
- let aa = a as m . RequestMessage ;
209
- if ( ! initialized && aa . method !== "initialize" ) {
249
+ } else if ( m . isRequestMessage ( msg ) ) {
250
+ // request message, aka client sent request and waits for our mandatory reply
251
+ if ( ! initialized && msg . method !== "initialize" ) {
210
252
let response : m . ResponseMessage = {
211
253
jsonrpc : c . jsonrpcVersion ,
212
- id : aa . id ,
254
+ id : msg . id ,
213
255
error : {
214
256
code : m . ErrorCodes . ServerNotInitialized ,
215
257
message : "Server not initialized." ,
216
258
} ,
217
259
} ;
218
260
process . send ! ( response ) ;
219
- } else if ( aa . method === "initialize" ) {
261
+ } else if ( msg . method === "initialize" ) {
220
262
// send the list of features we support
221
263
let result : p . InitializeResult = {
222
264
// This tells the client: "hey, we support the following operations".
@@ -232,25 +274,25 @@ process.on("message", (a: m.RequestMessage | m.NotificationMessage) => {
232
274
} ;
233
275
let response : m . ResponseMessage = {
234
276
jsonrpc : c . jsonrpcVersion ,
235
- id : aa . id ,
277
+ id : msg . id ,
236
278
result : result ,
237
279
} ;
238
280
initialized = true ;
239
281
process . send ! ( response ) ;
240
- } else if ( aa . method === "initialized" ) {
282
+ } else if ( msg . method === "initialized" ) {
241
283
// sent from client after initialize. Nothing to do for now
242
284
let response : m . ResponseMessage = {
243
285
jsonrpc : c . jsonrpcVersion ,
244
- id : aa . id ,
286
+ id : msg . id ,
245
287
result : null ,
246
288
} ;
247
289
process . send ! ( response ) ;
248
- } else if ( aa . method === "shutdown" ) {
290
+ } else if ( msg . method === "shutdown" ) {
249
291
// https://microsoft.github.io/language-server-protocol/specification#shutdown
250
292
if ( shutdownRequestAlreadyReceived ) {
251
293
let response : m . ResponseMessage = {
252
294
jsonrpc : c . jsonrpcVersion ,
253
- id : aa . id ,
295
+ id : msg . id ,
254
296
error : {
255
297
code : m . ErrorCodes . InvalidRequest ,
256
298
message : `Language server already received the shutdown request` ,
@@ -261,32 +303,33 @@ process.on("message", (a: m.RequestMessage | m.NotificationMessage) => {
261
303
shutdownRequestAlreadyReceived = true ;
262
304
// TODO: recheck logic around init/shutdown...
263
305
stopWatchingCompilerLog ( ) ;
306
+ // TODO: delete bsb watchers
264
307
265
308
let response : m . ResponseMessage = {
266
309
jsonrpc : c . jsonrpcVersion ,
267
- id : aa . id ,
310
+ id : msg . id ,
268
311
result : null ,
269
312
} ;
270
313
process . send ! ( response ) ;
271
314
}
272
- } else if ( aa . method === p . HoverRequest . method ) {
315
+ } else if ( msg . method === p . HoverRequest . method ) {
273
316
let dummyHoverResponse : m . ResponseMessage = {
274
317
jsonrpc : c . jsonrpcVersion ,
275
- id : aa . id ,
318
+ id : msg . id ,
276
319
// type result = Hover | null
277
320
// type Hover = {contents: MarkedString | MarkedString[] | MarkupContent, range?: Range}
278
321
result : { contents : "Time to go for a 20k run!" } ,
279
322
} ;
280
323
281
324
process . send ! ( dummyHoverResponse ) ;
282
- } else if ( aa . method === p . DefinitionRequest . method ) {
325
+ } else if ( msg . method === p . DefinitionRequest . method ) {
283
326
// https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_definition
284
327
let dummyDefinitionResponse : m . ResponseMessage = {
285
328
jsonrpc : c . jsonrpcVersion ,
286
- id : aa . id ,
329
+ id : msg . id ,
287
330
// result should be: Location | Array<Location> | Array<LocationLink> | null
288
331
result : {
289
- uri : aa . params . textDocument . uri ,
332
+ uri : msg . params . textDocument . uri ,
290
333
range : {
291
334
start : { line : 2 , character : 4 } ,
292
335
end : { line : 2 , character : 12 } ,
@@ -296,7 +339,7 @@ process.on("message", (a: m.RequestMessage | m.NotificationMessage) => {
296
339
} ;
297
340
298
341
process . send ! ( dummyDefinitionResponse ) ;
299
- } else if ( aa . method === p . DocumentFormattingRequest . method ) {
342
+ } else if ( msg . method === p . DocumentFormattingRequest . method ) {
300
343
// technically, a formatting failure should reply with the error. Sadly
301
344
// the LSP alert box for these error replies sucks (e.g. doesn't actually
302
345
// display the message). In order to signal the client to display a proper
@@ -306,11 +349,11 @@ process.on("message", (a: m.RequestMessage | m.NotificationMessage) => {
306
349
// nicer alert. Ugh.
307
350
let fakeSuccessResponse : m . ResponseMessage = {
308
351
jsonrpc : c . jsonrpcVersion ,
309
- id : aa . id ,
352
+ id : msg . id ,
310
353
result : [ ] ,
311
354
} ;
312
355
313
- let params = aa . params as p . DocumentFormattingParams ;
356
+ let params = msg . params as p . DocumentFormattingParams ;
314
357
let filePath = fileURLToPath ( params . textDocument . uri ) ;
315
358
let extension = path . extname ( params . textDocument . uri ) ;
316
359
if ( extension !== c . resExt && extension !== c . resiExt ) {
@@ -376,7 +419,7 @@ process.on("message", (a: m.RequestMessage | m.NotificationMessage) => {
376
419
] ;
377
420
let response : m . ResponseMessage = {
378
421
jsonrpc : c . jsonrpcVersion ,
379
- id : aa . id ,
422
+ id : msg . id ,
380
423
result : result ,
381
424
} ;
382
425
process . send ! ( response ) ;
@@ -393,13 +436,40 @@ process.on("message", (a: m.RequestMessage | m.NotificationMessage) => {
393
436
} else {
394
437
let response : m . ResponseMessage = {
395
438
jsonrpc : c . jsonrpcVersion ,
396
- id : aa . id ,
439
+ id : msg . id ,
397
440
error : {
398
441
code : m . ErrorCodes . InvalidRequest ,
399
442
message : "Unrecognized editor request." ,
400
443
} ,
401
444
} ;
402
445
process . send ! ( response ) ;
403
446
}
447
+ } else if ( m . isResponseMessage ( msg ) ) {
448
+ // response message. Currently the client should have only sent a response
449
+ // for asking us to start the build (see window/showMessageRequest in this
450
+ // file)
451
+
452
+ if (
453
+ msg . result != null &&
454
+ // @ts -ignore
455
+ msg . result . title != null &&
456
+ // @ts -ignore
457
+ msg . result . title === c . startBuildAction
458
+ ) {
459
+ let msg_ = msg . result as clientSentBuildAction ;
460
+ let projectRootPath = msg_ . projectRootPath ;
461
+ let bsbPath = path . join ( projectRootPath , c . bsbPartialPath ) ;
462
+ // TODO: sometime stale .bsb.lock dangling
463
+ // TODO: close watcher when lang-server shuts down
464
+ if ( fs . existsSync ( bsbPath ) ) {
465
+ let bsbProcess = utils . runBsbWatcherUsingValidBsbPath (
466
+ bsbPath ,
467
+ projectRootPath
468
+ ) ;
469
+ let root = projectsFiles . get ( projectRootPath ) ! ;
470
+ root . bsbWatcherByEditor = bsbProcess ;
471
+ bsbProcess . on ( "message" , ( a ) => console . log ( "wtf======" , a ) ) ;
472
+ }
473
+ }
404
474
}
405
475
} ) ;
0 commit comments