Skip to content

Commit 0c85f34

Browse files
committed
Diagnosis
1 parent cc86eaa commit 0c85f34

File tree

1 file changed

+193
-33
lines changed

1 file changed

+193
-33
lines changed

server/src/testserver.ts

Lines changed: 193 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,28 @@ import * as m from "vscode-jsonrpc/lib/messages";
77
import * as v from "vscode-languageserver";
88
import { attachPartialResult } from 'vscode-languageserver/lib/progress';
99
import * as path from 'path';
10-
import fs from 'fs';
10+
import fs, { open } from 'fs';
1111
import * as childProcess from 'child_process';
1212
import { DidOpenTextDocumentNotification, DidChangeTextDocumentNotification, DidCloseTextDocumentNotification } from 'vscode-languageserver-protocol';
1313
import * as tmp from 'tmp';
14+
import { type } from 'os';
15+
import { Range } from 'vscode-languageserver-textdocument';
16+
import { strict } from 'assert';
1417

1518
// See https://microsoft.github.io/language-server-protocol/specification Abstract Message
1619
// version is fixed to 2.0
17-
let jsonrpcVersion = '2.0'
18-
let bscPartialPath = path.join('node_modules', '.bin', 'bsc')
20+
let jsonrpcVersion = '2.0';
21+
let bscPartialPath = path.join('node_modules', '.bin', 'bsc');
22+
let bsbLogPartialPath = 'bsb.log';
23+
let resExt = '.res';
24+
let resiExt = '.resi';
1925

2026
// https://microsoft.github.io/language-server-protocol/specification#initialize
2127
// According to the spec, there could be requests before the 'initialize' request. Link in comment tells how to handle them.
2228
let initialized = false;
2329
// https://microsoft.github.io/language-server-protocol/specification#exit
2430
let shutdownRequestAlreadyReceived = false;
31+
let diagnosisTimer: null | NodeJS.Timeout = null;
2532

2633
// congrats. A simple UI problem is now a distributed system problem
2734
let stupidFileContentCache: { [key: string]: string } = {
@@ -53,7 +60,7 @@ let formatUsingValidBscPath = (code: string, bscPath: p.DocumentUri, isInterface
5360
let result;
5461
// library cleans up after itself. No need to manually remove temp file
5562
let tmpobj = tmp.fileSync();
56-
let extension = isInterface ? '.resi' : '.res';
63+
let extension = isInterface ? resiExt : resExt;
5764
let fileToFormat = tmpobj.name + extension;
5865
fs.writeFileSync(fileToFormat, code, { encoding: 'utf-8' });
5966
try {
@@ -70,13 +77,158 @@ let formatUsingValidBscPath = (code: string, bscPath: p.DocumentUri, isInterface
7077
}
7178
}
7279

73-
let startWatchingBsbOutputFile = (root: p.DocumentUri) => {
74-
console.log(root)
75-
// fs.watch()
80+
let parseBsbOutputLocation = (location: string): Range => {
81+
// example bsb output location:
82+
// 3:9
83+
// 3:9-6:1
84+
// language-server position is 0-based. Ours is 1-based. Don't forget to convert
85+
let isMultiline = location.indexOf('-') >= 0
86+
if (isMultiline) {
87+
let [from, to] = location.split('-')
88+
let [fromLine, fromChar] = from.split(':')
89+
let [toLine, toChar] = to.split(':')
90+
return {
91+
start: { line: parseInt(fromLine) - 1, character: parseInt(fromChar) },
92+
end: { line: parseInt(toLine) - 1, character: parseInt(toChar) },
93+
}
94+
} else {
95+
let [line, char] = location.split(':')
96+
let start = { line: parseInt(line) - 1, character: parseInt(char) - 1 }
97+
return {
98+
start: start,
99+
end: start,
100+
}
101+
}
102+
}
103+
type diagnosis = { range: Range, diagnosis: string }
104+
let parseBsbLogOutput = (content: string) => {
105+
/* example bsb.log file content:
106+
107+
Cleaning... 6 files.
108+
Cleaning... 87 files.
109+
[1/5] [34mBuilding[39m [2msrc/TestFramework.reiast[22m
110+
[2/5] [34mBuilding[39m [2msrc/TestFramework.reast[22m
111+
FAILED: src/test.cmj src/test.cmi
112+
113+
We've found a bug for you!
114+
/Users/chenglou/github/reason-react/src/test.res 3:9
115+
116+
1 │ let a = 1
117+
2 │ let b = "hi"
118+
3 │ let a = b + 1
119+
120+
This has type:
121+
string
122+
123+
But somewhere wanted:
124+
int
125+
126+
127+
[15/62] [34mBuilding[39m [2msrc/ReactDOMServer.reast[22m
128+
*/
129+
130+
// we're gonna chop that
131+
let res: string[][] = [];
132+
let lines = content.split('\n');
133+
lines.forEach(line => {
134+
if (line.startsWith(' We\'ve found a bug for you!')) {
135+
res.push([])
136+
} else if (/^ [0-9]+ /.test(line)) {
137+
// code display. Swallow
138+
} else if (line.startsWith(' ')) {
139+
res[res.length - 1].push(line)
140+
}
141+
})
142+
143+
// map of file path to list of diagnosis
144+
let ret: { [key: string]: diagnosis[] } = {}
145+
res.forEach(diagnosisLines => {
146+
let [fileAndLocation, ...diagnosisMessage] = diagnosisLines
147+
let lastSpace = fileAndLocation.lastIndexOf(' ')
148+
let file = fileAndLocation.substring(2, lastSpace)
149+
let location = fileAndLocation.substring(lastSpace)
150+
if (ret[file] == null) {
151+
ret[file] = []
152+
}
153+
let cleanedUpDiagnosis = diagnosisMessage
154+
.map(line => {
155+
// remove the spaces in front
156+
return line.slice(2)
157+
})
158+
.join('\n')
159+
// remove start and end whitespaces/newlines
160+
.trim();
161+
ret[file].push({
162+
range: parseBsbOutputLocation(location),
163+
diagnosis: cleanedUpDiagnosis,
164+
})
165+
})
166+
167+
return ret
168+
}
169+
170+
let startWatchingBsbOutputFile = (root: p.DocumentUri, process: NodeJS.Process) => {
171+
// console.log(root);
172+
// TOOD: setTimeout instead
173+
let id = setInterval(() => {
174+
let openFiles = Object.keys(stupidFileContentCache);
175+
let bsbLogDirs: Set<p.DocumentUri> = new Set();
176+
openFiles.forEach(openFile => {
177+
// TODO: remove this hack
178+
let filePath = openFile.replace('file://', '');
179+
let bsbLogDir = findDirOfFileNearFile(bsbLogPartialPath, filePath)
180+
if (bsbLogDir != null) {
181+
bsbLogDirs.add(bsbLogDir);
182+
}
183+
});
184+
185+
let files: { [key: string]: diagnosis[] } = {}
186+
187+
let res = Array.from(bsbLogDirs)
188+
.forEach(bsbLogDir => {
189+
let bsbLogPath = path.join(bsbLogDir, bsbLogPartialPath);
190+
let content = fs.readFileSync(bsbLogPath, { encoding: 'utf-8' });
191+
let filesAndErrors = parseBsbLogOutput(content)
192+
Object.keys(filesAndErrors).forEach(file => {
193+
files[file] = filesAndErrors[file]
194+
})
195+
// content.split('\n').forEach(line => {
196+
// let [file, diagnosis] = line.split(': ');
197+
198+
// if (files[file] == null) {
199+
// files[file] = []
200+
// }
201+
// files[file].push(diagnosis)
202+
// })
203+
});
204+
205+
Object.keys(files).forEach(file => {
206+
let params: p.PublishDiagnosticsParams = {
207+
uri: file,
208+
// there's a new optional version param from https://github.com/microsoft/language-server-protocol/issues/201
209+
// not using it for now, sigh
210+
diagnostics:
211+
files[file].map(({ range, diagnosis }) => {
212+
return {
213+
range: range,
214+
message: diagnosis,
215+
}
216+
}),
217+
}
218+
let notification: m.NotificationMessage = {
219+
jsonrpc: jsonrpcVersion,
220+
method: 'textDocument/publishDiagnostics',
221+
params: params,
222+
};
223+
process.send!(notification);
224+
})
225+
}, 1000);
226+
227+
return id;
228+
}
229+
let stopWatchingBsbOutputFile = (timerId: NodeJS.Timeout) => {
230+
clearInterval(timerId);
76231
}
77-
// let stopWatchingBsbOutputFile = () => {
78-
// fs.unwatchFile()
79-
// }
80232

81233
process.on('message', (a: (m.RequestMessage | m.NotificationMessage)) => {
82234
if ((a as m.RequestMessage).id == null) {
@@ -95,15 +247,21 @@ process.on('message', (a: (m.RequestMessage | m.NotificationMessage)) => {
95247
}
96248
} else if (aa.method === DidOpenTextDocumentNotification.method) {
97249
let params = (aa.params as p.DidOpenTextDocumentParams);
98-
stupidFileContentCache[params.textDocument.uri] = params.textDocument.text;
250+
let extName = path.extname(params.textDocument.uri)
251+
if (extName === resExt || extName === resiExt) {
252+
stupidFileContentCache[params.textDocument.uri] = params.textDocument.text;
253+
}
99254
} else if (aa.method === DidChangeTextDocumentNotification.method) {
100255
let params = (aa.params as p.DidChangeTextDocumentParams);
101-
let changes = params.contentChanges
102-
if (changes.length === 0) {
103-
// no change?
104-
} else {
105-
// we currently only support full changes
106-
stupidFileContentCache[params.textDocument.uri] = changes[changes.length - 1].text;
256+
let extName = path.extname(params.textDocument.uri)
257+
if (extName === resExt || extName === resiExt) {
258+
let changes = params.contentChanges
259+
if (changes.length === 0) {
260+
// no change?
261+
} else {
262+
// we currently only support full changes
263+
stupidFileContentCache[params.textDocument.uri] = changes[changes.length - 1].text;
264+
}
107265
}
108266
} else if (aa.method === DidCloseTextDocumentNotification.method) {
109267
let params = (aa.params as p.DidCloseTextDocumentParams);
@@ -121,15 +279,15 @@ process.on('message', (a: (m.RequestMessage | m.NotificationMessage)) => {
121279
message: "Server not initialized."
122280
}
123281
};
124-
(<any>process).send(response);
282+
process.send!(response);
125283
} else if (aa.method === 'initialize') {
126284
let param: p.InitializeParams = aa.params
127285
let root = param.rootUri
128286
if (root == null) {
129287
// TODO: handle single file
130288
console.log("not handling single file")
131289
} else {
132-
startWatchingBsbOutputFile(root)
290+
diagnosisTimer = startWatchingBsbOutputFile(root, process)
133291
}
134292
// send the list of things we support
135293
let result: p.InitializeResult = {
@@ -145,16 +303,15 @@ process.on('message', (a: (m.RequestMessage | m.NotificationMessage)) => {
145303
result: result,
146304
};
147305
initialized = true;
148-
(<any>process).send(response);
306+
process.send!(response);
149307
} else if (aa.method === 'initialized') {
150-
console.log('inited')
151308
// sent from client after initialize. Nothing to do for now
152309
let response: m.ResponseMessage = {
153310
jsonrpc: jsonrpcVersion,
154311
id: aa.id,
155312
result: null,
156313
};
157-
(<any>process).send(response);
314+
process.send!(response);
158315
} else if (aa.method === 'shutdown') {
159316
// https://microsoft.github.io/language-server-protocol/specification#shutdown
160317
if (shutdownRequestAlreadyReceived) {
@@ -166,31 +323,34 @@ process.on('message', (a: (m.RequestMessage | m.NotificationMessage)) => {
166323
message: `Language server already received the shutdown request`
167324
}
168325
};
169-
(<any>process).send(response);
326+
process.send!(response);
170327
} else {
171328
shutdownRequestAlreadyReceived = true
329+
if (diagnosisTimer != null) {
330+
stopWatchingBsbOutputFile(diagnosisTimer)
331+
}
172332
let response: m.ResponseMessage = {
173333
jsonrpc: jsonrpcVersion,
174334
id: aa.id,
175335
result: null,
176336
};
177-
(<any>process).send(response);
337+
process.send!(response);
178338
}
179339
} else if (aa.method === p.DocumentFormattingRequest.method) {
180340
let params = (aa.params as p.DocumentFormattingParams)
181341
// TODO: remove this hack
182-
let filePath = params.textDocument.uri.replace('file:', '')
342+
let filePath = params.textDocument.uri.replace('file://', '')
183343
let extension = path.extname(params.textDocument.uri);
184-
if (extension !== '.res' && extension !== '.resi') {
344+
if (extension !== resExt && extension !== resiExt) {
185345
let response: m.ResponseMessage = {
186346
jsonrpc: jsonrpcVersion,
187347
id: aa.id,
188348
error: {
189349
code: m.ErrorCodes.InvalidRequest,
190-
message: `Not a .res or .resi file.`
350+
message: `Not a ${resExt} or ${resiExt} file.`
191351
}
192352
};
193-
(<any>process).send(response);
353+
process.send!(response);
194354
} else {
195355
let nodeModulesParentPath = findDirOfFileNearFile(bscPartialPath, filePath)
196356
if (nodeModulesParentPath == null) {
@@ -202,7 +362,7 @@ process.on('message', (a: (m.RequestMessage | m.NotificationMessage)) => {
202362
message: `Cannot find a nearby ${bscPartialPath}. It's needed for formatting.`,
203363
}
204364
};
205-
(<any>process).send(response);
365+
process.send!(response);
206366
} else {
207367
// file to format potentially doesn't exist anymore because of races. But that's ok, the error from bsc should handle it
208368
let code = stupidFileContentCache[params.textDocument.uri];
@@ -213,7 +373,7 @@ process.on('message', (a: (m.RequestMessage | m.NotificationMessage)) => {
213373
let formattedResult = formatUsingValidBscPath(
214374
code,
215375
path.join(nodeModulesParentPath, bscPartialPath),
216-
extension === '.resi',
376+
extension === resiExt,
217377
);
218378
if (formattedResult.kind === 'success') {
219379
let result: p.TextEdit[] = [{
@@ -228,7 +388,7 @@ process.on('message', (a: (m.RequestMessage | m.NotificationMessage)) => {
228388
id: aa.id,
229389
result: result,
230390
};
231-
(<any>process).send(response);
391+
process.send!(response);
232392
} else {
233393
let response: m.ResponseMessage = {
234394
jsonrpc: jsonrpcVersion,
@@ -238,7 +398,7 @@ process.on('message', (a: (m.RequestMessage | m.NotificationMessage)) => {
238398
message: formattedResult.error,
239399
}
240400
};
241-
(<any>process).send(response);
401+
process.send!(response);
242402
}
243403
}
244404
}
@@ -252,7 +412,7 @@ process.on('message', (a: (m.RequestMessage | m.NotificationMessage)) => {
252412
message: "Unrecognized editor request."
253413
}
254414
};
255-
(<any>process).send(response);
415+
process.send!(response);
256416
}
257417
}
258418
})

0 commit comments

Comments
 (0)