Skip to content

Commit db5ea8c

Browse files
committed
move incremental compilation stuff to its own file
1 parent 32036d6 commit db5ea8c

File tree

4 files changed

+479
-306
lines changed

4 files changed

+479
-306
lines changed

server/src/config.ts

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { Message } from "vscode-languageserver-protocol";
2+
3+
export type send = (msg: Message) => void;
4+
5+
export interface extensionConfiguration {
6+
allowBuiltInFormatter: boolean;
7+
askToStartBuild: boolean;
8+
inlayHints: {
9+
enable: boolean;
10+
maxLength: number | null;
11+
};
12+
codeLens: boolean;
13+
binaryPath: string | null;
14+
platformPath: string | null;
15+
signatureHelp: {
16+
enabled: boolean;
17+
};
18+
incrementalTypechecking: {
19+
enabled: boolean;
20+
};
21+
}
22+
23+
// All values here are temporary, and will be overridden as the server is
24+
// initialized, and the current config is received from the client.
25+
let config: { extensionConfiguration: extensionConfiguration } = {
26+
extensionConfiguration: {
27+
allowBuiltInFormatter: false,
28+
askToStartBuild: true,
29+
inlayHints: {
30+
enable: false,
31+
maxLength: 25,
32+
},
33+
codeLens: false,
34+
binaryPath: null,
35+
platformPath: null,
36+
signatureHelp: {
37+
enabled: true,
38+
},
39+
incrementalTypechecking: {
40+
enabled: true,
41+
},
42+
},
43+
};
44+
45+
export default config;

server/src/incrementalCompilation.ts

+343
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,343 @@
1+
import * as path from "path";
2+
import fs from "fs";
3+
import * as utils from "./utils";
4+
import { pathToFileURL } from "url";
5+
import readline from "readline";
6+
import { performance } from "perf_hooks";
7+
import * as p from "vscode-languageserver-protocol";
8+
import * as cp from "node:child_process";
9+
import { send } from "./config";
10+
import * as c from "./constants";
11+
12+
/*
13+
* TODO CMT stuff
14+
* - Compile resi
15+
* - Wait a certain threshold for compilation before using the old cmt
16+
*/
17+
18+
let debug = true;
19+
20+
let buildNinjaCache: Map<string, [number, Array<string>]> = new Map();
21+
let savedIncrementalFiles: Set<string> = new Set();
22+
let compileContentsCache: Map<string, { timeout: any; triggerToken: number }> =
23+
new Map();
24+
let compileContentsListeners: Map<string, Array<() => void>> = new Map();
25+
26+
export function cleanupIncrementalFilesAfterCompilation(changedPath: string) {
27+
const projectRootPath = utils.findProjectRootOfFile(changedPath);
28+
if (projectRootPath != null) {
29+
savedIncrementalFiles.forEach((filePath) => {
30+
if (filePath.startsWith(projectRootPath)) {
31+
cleanUpIncrementalFiles(filePath, projectRootPath);
32+
savedIncrementalFiles.delete(filePath);
33+
}
34+
});
35+
}
36+
}
37+
38+
export function removeIncrementalFileFolder(
39+
projectRootPath: string,
40+
onAfterRemove?: () => void
41+
) {
42+
fs.rm(
43+
path.resolve(projectRootPath, "lib/bs/___incremental"),
44+
{ force: true, recursive: true },
45+
(_) => {
46+
onAfterRemove?.();
47+
}
48+
);
49+
}
50+
51+
export function recreateIncrementalFileFolder(projectRootPath: string) {
52+
removeIncrementalFileFolder(projectRootPath, () => {
53+
fs.mkdir(path.resolve(projectRootPath, "lib/bs/___incremental"), (_) => {});
54+
});
55+
}
56+
57+
export function fileIsIncrementallyCompiled(filePath: string): boolean {
58+
let projectRootPath = utils.findProjectRootOfFile(filePath);
59+
let fileName = path.basename(filePath, ".res");
60+
if (projectRootPath != null) {
61+
return fs.existsSync(
62+
path.resolve(projectRootPath, "lib/bs/___incremental", fileName + ".cmt")
63+
);
64+
}
65+
return false;
66+
}
67+
68+
export function cleanUpIncrementalFiles(
69+
filePath: string,
70+
projectRootPath: string
71+
) {
72+
[
73+
path.basename(filePath, ".res") + ".ast",
74+
path.basename(filePath, ".res") + ".cmt",
75+
path.basename(filePath, ".res") + ".cmi",
76+
path.basename(filePath, ".res") + ".cmj",
77+
path.basename(filePath),
78+
].forEach((file) => {
79+
fs.unlink(
80+
path.resolve(projectRootPath, "lib/bs/___incremental", file),
81+
(_) => {}
82+
);
83+
});
84+
}
85+
function getBscArgs(projectRootPath: string): Promise<Array<string>> {
86+
let buildNinjaPath = path.resolve(projectRootPath, "lib/bs/build.ninja");
87+
let cacheEntry = buildNinjaCache.get(buildNinjaPath);
88+
let stat: fs.Stats | null = null;
89+
if (cacheEntry != null) {
90+
stat = fs.statSync(buildNinjaPath);
91+
if (cacheEntry[0] >= stat.mtimeMs) {
92+
return Promise.resolve(cacheEntry[1]);
93+
}
94+
}
95+
return new Promise((resolve, _reject) => {
96+
function resolveResult(result: Array<string>) {
97+
if (stat != null) {
98+
buildNinjaCache.set(buildNinjaPath, [stat.mtimeMs, result]);
99+
}
100+
resolve(result);
101+
}
102+
const fileStream = fs.createReadStream(
103+
path.resolve(projectRootPath, "lib/bs/build.ninja")
104+
);
105+
const rl = readline.createInterface({
106+
input: fileStream,
107+
crlfDelay: Infinity,
108+
});
109+
let captureNextLine = false;
110+
let done = false;
111+
let stopped = false;
112+
let captured: Array<string> = [];
113+
rl.on("line", (line) => {
114+
if (stopped) {
115+
return;
116+
}
117+
if (captureNextLine) {
118+
captured.push(line);
119+
captureNextLine = false;
120+
}
121+
if (done) {
122+
fileStream.destroy();
123+
rl.close();
124+
resolveResult(captured);
125+
stopped = true;
126+
return;
127+
}
128+
if (line.startsWith("rule astj")) {
129+
captureNextLine = true;
130+
}
131+
if (line.startsWith("rule mij")) {
132+
captureNextLine = true;
133+
done = true;
134+
}
135+
});
136+
rl.on("close", () => {
137+
resolveResult(captured);
138+
});
139+
});
140+
}
141+
function argsFromCommandString(cmdString: string): Array<Array<string>> {
142+
let s = cmdString
143+
.trim()
144+
.split("command = ")[1]
145+
.split(" ")
146+
.map((v) => v.trim())
147+
.filter((v) => v !== "");
148+
let args: Array<Array<string>> = [];
149+
150+
for (let i = 0; i <= s.length - 1; i++) {
151+
let item = s[i];
152+
let nextItem = s[i + 1] ?? "";
153+
if (item.startsWith("-") && nextItem.startsWith("-")) {
154+
// Single entry arg
155+
args.push([item]);
156+
} else if (item.startsWith("-") && s[i + 1]?.startsWith("'")) {
157+
let nextIndex = i + 1;
158+
// Quoted arg, take until ending '
159+
let arg = [s[nextIndex]];
160+
for (let x = nextIndex + 1; x <= s.length - 1; x++) {
161+
let nextItem = s[x];
162+
arg.push(nextItem);
163+
if (nextItem.endsWith("'")) {
164+
i = x + 1;
165+
break;
166+
}
167+
}
168+
args.push([item, arg.join(" ")]);
169+
} else if (item.startsWith("-")) {
170+
args.push([item, nextItem]);
171+
}
172+
}
173+
return args;
174+
}
175+
function removeAnsiCodes(s: string): string {
176+
const ansiEscape = /\x1B[@-_][0-?]*[ -/]*[@-~]/g;
177+
return s.replace(ansiEscape, "");
178+
}
179+
function triggerIncrementalCompilationOfFile(
180+
filePath: string,
181+
fileContent: string,
182+
send: send,
183+
onCompilationFinished?: () => void
184+
) {
185+
let cacheEntry = compileContentsCache.get(filePath);
186+
if (cacheEntry != null) {
187+
clearTimeout(cacheEntry.timeout);
188+
compileContentsListeners.get(filePath)?.forEach((cb) => cb());
189+
compileContentsListeners.delete(filePath);
190+
}
191+
let triggerToken = performance.now();
192+
compileContentsCache.set(filePath, {
193+
timeout: setTimeout(() => {
194+
compileContents(
195+
filePath,
196+
fileContent,
197+
triggerToken,
198+
send,
199+
onCompilationFinished
200+
);
201+
}, 20),
202+
triggerToken,
203+
});
204+
}
205+
function verifyTriggerToken(filePath: string, triggerToken: number): boolean {
206+
return compileContentsCache.get(filePath)?.triggerToken === triggerToken;
207+
}
208+
async function compileContents(
209+
filePath: string,
210+
fileContent: string,
211+
triggerToken: number,
212+
send: (msg: p.Message) => void,
213+
onCompilationFinished?: () => void
214+
) {
215+
let startTime = performance.now();
216+
let fileName = path.basename(filePath);
217+
const projectRootPath = utils.findProjectRootOfFile(filePath);
218+
if (projectRootPath == null) {
219+
if (debug) console.log("Did not find root project.");
220+
return;
221+
}
222+
let bscExe = utils.findBscExeBinary(projectRootPath);
223+
if (bscExe == null) {
224+
if (debug) console.log("Did not find bsc.");
225+
return;
226+
}
227+
let incrementalFilePath = path.resolve(
228+
projectRootPath,
229+
"lib/bs/___incremental",
230+
fileName
231+
);
232+
233+
fs.writeFileSync(incrementalFilePath, fileContent);
234+
235+
try {
236+
let [astBuildCommand, fullBuildCommand] = await getBscArgs(projectRootPath);
237+
238+
let astArgs = argsFromCommandString(astBuildCommand);
239+
let buildArgs = argsFromCommandString(fullBuildCommand);
240+
241+
let callArgs: Array<string> = [
242+
"-I",
243+
path.resolve(projectRootPath, "lib/bs/___incremental"),
244+
];
245+
246+
buildArgs.forEach(([key, value]: Array<string>) => {
247+
if (key === "-I") {
248+
callArgs.push("-I", path.resolve(projectRootPath, "lib/bs", value));
249+
} else if (key === "-bs-v") {
250+
callArgs.push("-bs-v", Date.now().toString());
251+
} else if (key === "-bs-package-output") {
252+
return;
253+
} else if (value == null || value === "") {
254+
callArgs.push(key);
255+
} else {
256+
callArgs.push(key, value);
257+
}
258+
});
259+
260+
astArgs.forEach(([key, value]: Array<string>) => {
261+
if (key.startsWith("-bs-jsx")) {
262+
callArgs.push(key, value);
263+
} else if (key.startsWith("-ppx")) {
264+
callArgs.push(key, value);
265+
}
266+
});
267+
268+
callArgs.push("-color", "never");
269+
callArgs.push("-ignore-parse-errors");
270+
271+
callArgs = callArgs.filter((v) => v != null && v !== "");
272+
callArgs.push(incrementalFilePath);
273+
274+
let process = cp.execFile(
275+
bscExe,
276+
callArgs,
277+
{ cwd: projectRootPath },
278+
(error, _stdout, stderr) => {
279+
if (!error?.killed) {
280+
if (debug)
281+
console.log(
282+
`Recompiled ${fileName} in ${
283+
(performance.now() - startTime) / 1000
284+
}s`
285+
);
286+
} else {
287+
if (debug) console.log(`Compilation of ${fileName} was killed.`);
288+
}
289+
onCompilationFinished?.();
290+
if (!error?.killed && verifyTriggerToken(filePath, triggerToken)) {
291+
let { result } = utils.parseCompilerLogOutput(`${stderr}\n#Done()`);
292+
let res = (Object.values(result)[0] ?? [])
293+
.map((d) => ({
294+
...d,
295+
message: removeAnsiCodes(d.message),
296+
}))
297+
// Filter out a few unwanted parser errors since we run the parser in ignore mode
298+
.filter(
299+
(d) =>
300+
!d.message.startsWith("Uninterpreted extension 'rescript.") &&
301+
!d.message.includes(`/___incremental/${fileName}`)
302+
);
303+
304+
let notification: p.NotificationMessage = {
305+
jsonrpc: c.jsonrpcVersion,
306+
method: "textDocument/publishDiagnostics",
307+
params: {
308+
uri: pathToFileURL(filePath),
309+
diagnostics: res,
310+
},
311+
};
312+
send(notification);
313+
}
314+
}
315+
);
316+
let listeners = compileContentsListeners.get(filePath) ?? [];
317+
listeners.push(() => {
318+
process.kill("SIGKILL");
319+
});
320+
compileContentsListeners.set(filePath, listeners);
321+
} catch (e) {
322+
console.error(e);
323+
}
324+
}
325+
326+
export function handleUpdateOpenedFile(
327+
filePath: string,
328+
fileContent: string,
329+
send: send,
330+
onCompilationFinished?: () => void
331+
) {
332+
savedIncrementalFiles.delete(filePath);
333+
triggerIncrementalCompilationOfFile(
334+
filePath,
335+
fileContent,
336+
send,
337+
onCompilationFinished
338+
);
339+
}
340+
341+
export function handleDidSaveTextDocument(filePath: string) {
342+
savedIncrementalFiles.add(filePath);
343+
}

0 commit comments

Comments
 (0)