@@ -19,10 +19,12 @@ import * as c from "./constants";
1919import * as chokidar from "chokidar" ;
2020import { assert } from "console" ;
2121import { fileURLToPath } from "url" ;
22+ import { ChildProcess } from "child_process" ;
2223
2324// https://microsoft.github.io/language-server-protocol/specification#initialize
2425// According to the spec, there could be requests before the 'initialize' request. Link in comment tells how to handle them.
2526let initialized = false ;
27+ let serverSentRequestIdCounter = 0 ;
2628// https://microsoft.github.io/language-server-protocol/specification#exit
2729let shutdownRequestAlreadyReceived = false ;
2830let stupidFileContentCache : Map < string , string > = new Map ( ) ;
@@ -31,6 +33,7 @@ let projectsFiles: Map<
3133 {
3234 openFiles : Set < string > ;
3335 filesWithDiagnostics : Set < string > ;
36+ bsbWatcherByEditor : null | ChildProcess ;
3437 }
3538> = new Map ( ) ;
3639// ^ caching AND states AND distributed system. Why does LSP has to be stupid like this
@@ -109,6 +112,10 @@ let stopWatchingCompilerLog = () => {
109112 compilerLogsWatcher . close ( ) ;
110113} ;
111114
115+ type clientSentBuildAction = {
116+ title : string ;
117+ projectRootPath : string ;
118+ } ;
112119let openedFile = ( fileUri : string , fileContent : string ) => {
113120 let filePath = fileURLToPath ( fileUri ) ;
114121
@@ -120,13 +127,46 @@ let openedFile = (fileUri: string, fileContent: string) => {
120127 projectsFiles . set ( projectRootPath , {
121128 openFiles : new Set ( ) ,
122129 filesWithDiagnostics : new Set ( ) ,
130+ bsbWatcherByEditor : null ,
123131 } ) ;
124132 compilerLogsWatcher . add (
125133 path . join ( projectRootPath , c . compilerLogPartialPath )
126134 ) ;
127135 }
128136 let root = projectsFiles . get ( projectRootPath ) ! ;
129137 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+
130170 // no need to call sendUpdatedDiagnostics() here; the watcher add will
131171 // call the listener which calls it
132172 }
@@ -147,6 +187,10 @@ let closedFile = (fileUri: string) => {
147187 path . join ( projectRootPath , c . compilerLogPartialPath )
148188 ) ;
149189 deleteProjectDiagnostics ( projectRootPath ) ;
190+ if ( root . bsbWatcherByEditor !== null ) {
191+ root . bsbWatcherByEditor . kill ( ) ;
192+ root . bsbWatcherByEditor = null ;
193+ }
150194 }
151195 }
152196 }
@@ -163,29 +207,28 @@ let getOpenedFileContent = (fileUri: string) => {
163207 return content ;
164208} ;
165209
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" ) {
171214 // From spec: "Notifications should be dropped, except for the exit notification. This will allow the exit of a server without an initialize request"
172215 // For us: do nothing. We don't have anything we need to clean up right now
173216 // 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" ) {
175218 // The server should exit with success code 0 if the shutdown request has been received before; otherwise with error code 1
176219 if ( shutdownRequestAlreadyReceived ) {
177220 process . exit ( 0 ) ;
178221 } else {
179222 process . exit ( 1 ) ;
180223 }
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 ;
183226 let extName = path . extname ( params . textDocument . uri ) ;
184227 if ( extName === c . resExt || extName === c . resiExt ) {
185228 openedFile ( params . textDocument . uri , params . textDocument . text ) ;
186229 }
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 ;
189232 let extName = path . extname ( params . textDocument . uri ) ;
190233 if ( extName === c . resExt || extName === c . resiExt ) {
191234 let changes = params . contentChanges ;
@@ -199,24 +242,23 @@ process.on("message", (a: m.RequestMessage | m.NotificationMessage) => {
199242 ) ;
200243 }
201244 }
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 ;
204247 closedFile ( params . textDocument . uri ) ;
205248 }
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" ) {
210252 let response : m . ResponseMessage = {
211253 jsonrpc : c . jsonrpcVersion ,
212- id : aa . id ,
254+ id : msg . id ,
213255 error : {
214256 code : m . ErrorCodes . ServerNotInitialized ,
215257 message : "Server not initialized." ,
216258 } ,
217259 } ;
218260 process . send ! ( response ) ;
219- } else if ( aa . method === "initialize" ) {
261+ } else if ( msg . method === "initialize" ) {
220262 // send the list of features we support
221263 let result : p . InitializeResult = {
222264 // This tells the client: "hey, we support the following operations".
@@ -232,25 +274,25 @@ process.on("message", (a: m.RequestMessage | m.NotificationMessage) => {
232274 } ;
233275 let response : m . ResponseMessage = {
234276 jsonrpc : c . jsonrpcVersion ,
235- id : aa . id ,
277+ id : msg . id ,
236278 result : result ,
237279 } ;
238280 initialized = true ;
239281 process . send ! ( response ) ;
240- } else if ( aa . method === "initialized" ) {
282+ } else if ( msg . method === "initialized" ) {
241283 // sent from client after initialize. Nothing to do for now
242284 let response : m . ResponseMessage = {
243285 jsonrpc : c . jsonrpcVersion ,
244- id : aa . id ,
286+ id : msg . id ,
245287 result : null ,
246288 } ;
247289 process . send ! ( response ) ;
248- } else if ( aa . method === "shutdown" ) {
290+ } else if ( msg . method === "shutdown" ) {
249291 // https://microsoft.github.io/language-server-protocol/specification#shutdown
250292 if ( shutdownRequestAlreadyReceived ) {
251293 let response : m . ResponseMessage = {
252294 jsonrpc : c . jsonrpcVersion ,
253- id : aa . id ,
295+ id : msg . id ,
254296 error : {
255297 code : m . ErrorCodes . InvalidRequest ,
256298 message : `Language server already received the shutdown request` ,
@@ -261,32 +303,33 @@ process.on("message", (a: m.RequestMessage | m.NotificationMessage) => {
261303 shutdownRequestAlreadyReceived = true ;
262304 // TODO: recheck logic around init/shutdown...
263305 stopWatchingCompilerLog ( ) ;
306+ // TODO: delete bsb watchers
264307
265308 let response : m . ResponseMessage = {
266309 jsonrpc : c . jsonrpcVersion ,
267- id : aa . id ,
310+ id : msg . id ,
268311 result : null ,
269312 } ;
270313 process . send ! ( response ) ;
271314 }
272- } else if ( aa . method === p . HoverRequest . method ) {
315+ } else if ( msg . method === p . HoverRequest . method ) {
273316 let dummyHoverResponse : m . ResponseMessage = {
274317 jsonrpc : c . jsonrpcVersion ,
275- id : aa . id ,
318+ id : msg . id ,
276319 // type result = Hover | null
277320 // type Hover = {contents: MarkedString | MarkedString[] | MarkupContent, range?: Range}
278321 result : { contents : "Time to go for a 20k run!" } ,
279322 } ;
280323
281324 process . send ! ( dummyHoverResponse ) ;
282- } else if ( aa . method === p . DefinitionRequest . method ) {
325+ } else if ( msg . method === p . DefinitionRequest . method ) {
283326 // https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_definition
284327 let dummyDefinitionResponse : m . ResponseMessage = {
285328 jsonrpc : c . jsonrpcVersion ,
286- id : aa . id ,
329+ id : msg . id ,
287330 // result should be: Location | Array<Location> | Array<LocationLink> | null
288331 result : {
289- uri : aa . params . textDocument . uri ,
332+ uri : msg . params . textDocument . uri ,
290333 range : {
291334 start : { line : 2 , character : 4 } ,
292335 end : { line : 2 , character : 12 } ,
@@ -296,7 +339,7 @@ process.on("message", (a: m.RequestMessage | m.NotificationMessage) => {
296339 } ;
297340
298341 process . send ! ( dummyDefinitionResponse ) ;
299- } else if ( aa . method === p . DocumentFormattingRequest . method ) {
342+ } else if ( msg . method === p . DocumentFormattingRequest . method ) {
300343 // technically, a formatting failure should reply with the error. Sadly
301344 // the LSP alert box for these error replies sucks (e.g. doesn't actually
302345 // 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) => {
306349 // nicer alert. Ugh.
307350 let fakeSuccessResponse : m . ResponseMessage = {
308351 jsonrpc : c . jsonrpcVersion ,
309- id : aa . id ,
352+ id : msg . id ,
310353 result : [ ] ,
311354 } ;
312355
313- let params = aa . params as p . DocumentFormattingParams ;
356+ let params = msg . params as p . DocumentFormattingParams ;
314357 let filePath = fileURLToPath ( params . textDocument . uri ) ;
315358 let extension = path . extname ( params . textDocument . uri ) ;
316359 if ( extension !== c . resExt && extension !== c . resiExt ) {
@@ -376,7 +419,7 @@ process.on("message", (a: m.RequestMessage | m.NotificationMessage) => {
376419 ] ;
377420 let response : m . ResponseMessage = {
378421 jsonrpc : c . jsonrpcVersion ,
379- id : aa . id ,
422+ id : msg . id ,
380423 result : result ,
381424 } ;
382425 process . send ! ( response ) ;
@@ -393,13 +436,40 @@ process.on("message", (a: m.RequestMessage | m.NotificationMessage) => {
393436 } else {
394437 let response : m . ResponseMessage = {
395438 jsonrpc : c . jsonrpcVersion ,
396- id : aa . id ,
439+ id : msg . id ,
397440 error : {
398441 code : m . ErrorCodes . InvalidRequest ,
399442 message : "Unrecognized editor request." ,
400443 } ,
401444 } ;
402445 process . send ! ( response ) ;
403446 }
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+ }
404474 }
405475} ) ;
0 commit comments