Skip to content

Commit 4d93eb8

Browse files
committed
Test bsb watcher
1 parent 740e578 commit 4d93eb8

File tree

3 files changed

+129
-36
lines changed

3 files changed

+129
-36
lines changed

server/src/constants.ts

+5
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,12 @@ import * as path from 'path';
44
// version is fixed to 2.0
55
export let jsonrpcVersion = '2.0';
66
export let bscPartialPath = path.join('node_modules', 'bs-platform', process.platform, 'bsc.exe');
7+
// can't use the native bsb since we might need the watcher -w flag, which is only in the js wrapper
8+
// export let bsbPartialPath = path.join('node_modules', 'bs-platform', process.platform, 'bsb.exe');
9+
export let bsbPartialPath = path.join('node_modules', '.bin', 'bsb');
10+
export let bsbLock = '.bsb.lock';
711
export let bsconfigPartialPath = 'bsconfig.json';
812
export let compilerLogPartialPath = path.join('lib', 'bs', '.compiler.log');
913
export let resExt = '.res';
1014
export let resiExt = '.resi';
15+
export let startBuildAction = 'Start Build';

server/src/server.ts

+104-34
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,12 @@ import * as c from "./constants";
1919
import * as chokidar from "chokidar";
2020
import { assert } from "console";
2121
import { 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.
2526
let initialized = false;
27+
let serverSentRequestIdCounter = 0;
2628
// https://microsoft.github.io/language-server-protocol/specification#exit
2729
let shutdownRequestAlreadyReceived = false;
2830
let 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+
};
112119
let 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
});

server/src/utils.ts

+20-2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import * as path from 'path';
66
import * as t from "vscode-languageserver-types";
77
import * as tmp from 'tmp';
88
import fs from 'fs';
9+
import { report } from 'process';
910

1011
// TODO: races here
1112
// TODO: this doesn't handle file:/// scheme
@@ -23,14 +24,14 @@ export let findProjectRootOfFile = (source: p.DocumentUri): null | p.DocumentUri
2324
}
2425
}
2526

26-
type formattingResult = {
27+
type execResult = {
2728
kind: 'success',
2829
result: string
2930
} | {
3031
kind: 'error'
3132
error: string,
3233
};
33-
export let formatUsingValidBscPath = (code: string, bscPath: p.DocumentUri, isInterface: boolean): formattingResult => {
34+
export let formatUsingValidBscPath = (code: string, bscPath: p.DocumentUri, isInterface: boolean): execResult => {
3435
// library cleans up after itself. No need to manually remove temp file
3536
let tmpobj = tmp.fileSync();
3637
let extension = isInterface ? c.resiExt : c.resExt;
@@ -50,6 +51,23 @@ export let formatUsingValidBscPath = (code: string, bscPath: p.DocumentUri, isIn
5051
}
5152
}
5253

54+
export let runBsbWatcherUsingValidBsbPath = (bsbPath: p.DocumentUri, projectRootPath: p.DocumentUri) => {
55+
let process = childProcess.execFile(bsbPath, ['-w'], { cwd: projectRootPath })
56+
return process
57+
// try {
58+
// let result = childProcess.execFileSync(bsbPath, [], { stdio: 'pipe', cwd: projectRootPath })
59+
// return {
60+
// kind: 'success',
61+
// result: result.toString(),
62+
// }
63+
// } catch (e) {
64+
// return {
65+
// kind: 'error',
66+
// error: e.message,
67+
// }
68+
// }
69+
}
70+
5371
export let parseDiagnosticLocation = (location: string): Range => {
5472
// example output location:
5573
// 3:9

0 commit comments

Comments
 (0)