Skip to content

Commit 6ec3d03

Browse files
committed
WIP compiler API
1 parent 83c6fbc commit 6ec3d03

File tree

11 files changed

+208
-135
lines changed

11 files changed

+208
-135
lines changed

src/compiler/program.ts

+100-55
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import fs from "fs";
22
import globby from "globby";
33
import os from "os";
4+
import "reflect-metadata";
45
import { container } from "tsyringe";
56
import util from "util";
6-
import { Connection } from "vscode-languageserver";
77
import { URI } from "vscode-uri";
88
import Parser, { Tree } from "web-tree-sitter";
99
import { ICancellationToken } from "../cancellation";
@@ -17,7 +17,7 @@ import {
1717
IPossibleImportsCache,
1818
PossibleImportsCache,
1919
} from "../util/possibleImportsCache";
20-
import { Settings } from "../util/settings";
20+
import { getDefaultSettings, IClientSettings } from "../util/settings";
2121
import { Diagnostic } from "./diagnostics";
2222
import { TypeCache } from "./typeCache";
2323
import {
@@ -26,7 +26,8 @@ import {
2626
TypeChecker,
2727
} from "./typeChecker";
2828
import chokidar from "chokidar";
29-
import { CommandManager } from "../commandManager";
29+
import { Connection } from "vscode-languageserver";
30+
import { loadParser } from "../parser";
3031

3132
const readFile = util.promisify(fs.readFile);
3233

@@ -87,6 +88,7 @@ export interface IProgram {
8788
sourceFile: ISourceFile,
8889
importableModuleName: string,
8990
): ISourceFile | undefined;
91+
getSourceFiles(): ISourceFile[];
9092
getForest(synchronize?: boolean): IForest;
9193
getRootPath(): URI;
9294
getTypeCache(): TypeCache;
@@ -135,16 +137,39 @@ interface IElmPackage extends IElmProject {
135137
exposedModules: Set<string>;
136138
}
137139

140+
export async function createProgram(
141+
rootPath: URI,
142+
progressCallback?: (percent: number) => void,
143+
programHost?: IProgramHost,
144+
settings?: IClientSettings,
145+
): Promise<IProgram> {
146+
const program = new Program(rootPath, programHost, settings);
147+
await program.init(
148+
progressCallback ??
149+
((): void => {
150+
//
151+
}),
152+
);
153+
return program;
154+
}
155+
138156
export interface IProgramHost {
139157
readFile(uri: string): Promise<string>;
140158
readDirectory(uri: string): Promise<string[]>;
141159
watchFile(uri: string, callback: () => void): void;
160+
logger: {
161+
info(message: string): void;
162+
warn(message: string): void;
163+
error(message: string): void;
164+
};
165+
handleError(message: string): void;
166+
onServerDidRestart(
167+
handler: (progressReporter: (progress: number) => void) => Promise<void>,
168+
): void;
142169
}
143170

144171
export class Program implements IProgram {
145-
private parser: Parser;
146-
private connection: Connection;
147-
private settings: Settings;
172+
private parser!: Parser;
148173
private typeCache: TypeCache;
149174
private typeChecker: TypeChecker | undefined;
150175
private dirty = true;
@@ -157,24 +182,27 @@ export class Program implements IProgram {
157182
private resolvedPackageCache = new Map<string, IElmPackage>();
158183
private host: IProgramHost;
159184
private filesWatching = new Set<string>();
185+
private settings: IClientSettings;
160186

161-
constructor(private rootPath: URI, programHost?: IProgramHost) {
162-
this.settings = container.resolve("Settings");
163-
this.connection = container.resolve("Connection");
164-
this.parser = container.resolve("Parser");
165-
this.connection.console.info(
166-
`Starting language server for folder: ${this.rootPath.toString()}`,
167-
);
168-
187+
constructor(
188+
private rootPath: URI,
189+
programHost?: IProgramHost,
190+
settings?: IClientSettings,
191+
) {
169192
this.typeCache = new TypeCache();
170193
this.possibleImportsCache = new PossibleImportsCache();
171194
this.operatorsCache = new Map<string, DefinitionResult>();
172195
this.diagnosticsCache = new Map<string, Diagnostic[]>();
173196
this.host = programHost ?? createNodeProgramHost();
197+
this.settings = settings ?? getDefaultSettings();
198+
199+
this.host.logger.info(
200+
`Starting language server for folder: ${this.rootPath.toString()}`,
201+
);
174202
}
175203

176204
public async init(
177-
progressCallback: (percent: number) => void,
205+
progressCallback?: (percent: number) => void,
178206
): Promise<void> {
179207
await this.initWorkspace(progressCallback);
180208
}
@@ -215,6 +243,10 @@ export class Program implements IProgram {
215243
}
216244
}
217245

246+
public getSourceFiles(): ISourceFile[] {
247+
return Array.from(this.getForest().treeMap.values());
248+
}
249+
218250
public getForest(synchronize = true): IForest {
219251
if (this.dirty && synchronize) {
220252
this.forest.synchronize();
@@ -238,7 +270,10 @@ export class Program implements IProgram {
238270
this.dirty = false;
239271
}
240272

241-
return this.typeChecker ?? (this.typeChecker = createTypeChecker(this));
273+
return (
274+
this.typeChecker ??
275+
(this.typeChecker = createTypeChecker(this, this.host))
276+
);
242277
}
243278

244279
public markAsDirty(): void {
@@ -323,41 +358,34 @@ export class Program implements IProgram {
323358
}
324359

325360
private async initWorkspace(
326-
progressCallback: (percent: number) => void,
361+
progressCallback?: (percent: number) => void,
327362
): Promise<void> {
328-
const clientSettings = await this.settings.getClientSettings();
363+
if (!container.isRegistered("Parser")) {
364+
await loadParser(this.host);
365+
}
366+
367+
this.parser = container.resolve("Parser");
368+
329369
let progress = 0;
330370
let elmVersion;
331371
try {
332-
elmVersion = utils.getElmVersion(
333-
clientSettings,
334-
this.rootPath,
335-
this.connection,
336-
);
372+
elmVersion = utils.getElmVersion(this.settings, this.rootPath, this.host);
337373
} catch (error) {
338-
this.connection.console.warn(
374+
this.host.logger.warn(
339375
`Could not figure out elm version, this will impact how good the server works. \n ${error.stack}`,
340376
);
341377
}
342378

343379
const pathToElmJson = path.join(this.rootPath.fsPath, "elm.json");
344-
this.connection.console.info(`Reading elm.json from ${pathToElmJson}`);
380+
this.host.logger.info(`Reading elm.json from ${pathToElmJson}`);
345381

346382
if (!this.filesWatching.has(pathToElmJson)) {
347383
this.host.watchFile(pathToElmJson, () => {
348-
void this.connection.window
349-
.createWorkDoneProgress()
350-
.then((progress) => {
351-
progress.begin("Restarting Elm Language Server", 0);
352-
353-
this.initWorkspace((percent: number) => {
354-
progress.report(percent, `${percent.toFixed(0)}%`);
355-
})
356-
.then(() => progress.done())
357-
.catch(() => {
358-
//
359-
});
384+
this.host.onServerDidRestart(async (progressReporter) => {
385+
await this.initWorkspace((percent: number) => {
386+
progressReporter(percent);
360387
});
388+
});
361389
});
362390
this.filesWatching.add(pathToElmJson);
363391
}
@@ -371,11 +399,11 @@ export class Program implements IProgram {
371399
// Run `elm make` to download dependencies
372400
try {
373401
utils.execCmdSync(
374-
clientSettings.elmPath,
402+
this.settings.elmPath,
375403
"elm",
376404
{ cmdArguments: ["make"] },
377405
this.rootPath.fsPath,
378-
this.connection,
406+
this.host,
379407
);
380408
} catch (error) {
381409
// On application projects, this will give a NO INPUT error message, but will still download the dependencies
@@ -386,38 +414,38 @@ export class Program implements IProgram {
386414
this.forest = new Forest(this.rootProject);
387415

388416
const elmFilePaths = await this.findElmFilesInProject(this.rootProject);
389-
this.connection.console.info(
417+
this.host.logger.info(
390418
`Found ${elmFilePaths.length.toString()} files to add to the project`,
391419
);
392420

393421
if (elmFilePaths.every((a) => a.project !== this.rootProject)) {
394-
this.connection.window.showErrorMessage(
422+
this.host.handleError(
395423
"The path or paths you entered in the 'source-directories' field of your 'elm.json' does not contain any elm files.",
396424
);
397425
}
398426

399427
const promiseList: Promise<void>[] = [];
400-
const PARSE_STAGES = 3;
428+
const PARSE_STAGES = 2;
401429
const progressDelta = 100 / (elmFilePaths.length * PARSE_STAGES);
402430
for (const filePath of elmFilePaths) {
403-
progressCallback((progress += progressDelta));
431+
if (progressCallback) {
432+
progressCallback((progress += progressDelta));
433+
}
404434
promiseList.push(
405435
this.readAndAddToForest(filePath, () => {
406-
progressCallback((progress += progressDelta));
436+
if (progressCallback) {
437+
progressCallback((progress += progressDelta));
438+
}
407439
}),
408440
);
409441
}
410442
await Promise.all(promiseList);
411443

412444
this.findExposedModulesOfDependencies(this.rootProject);
413445

414-
CommandManager.initHandlers(this.connection);
415-
416-
this.connection.console.info(
417-
`Done parsing all files for ${pathToElmJson}`,
418-
);
446+
this.host.logger.info(`Done parsing all files for ${pathToElmJson}`);
419447
} catch (error) {
420-
this.connection.console.error(
448+
this.host.logger.error(
421449
`Error parsing files for ${pathToElmJson}:\n${error.stack}`,
422450
);
423451
}
@@ -467,7 +495,7 @@ export class Program implements IProgram {
467495
);
468496

469497
if (!solvedVersions) {
470-
this.connection.window.showErrorMessage(
498+
this.host.handleError(
471499
"There is a problem with elm.json. Could not solve dependencies with the given constraints. Try running `elm make` to install missing dependencies.",
472500
);
473501
throw new Error("Unsolvable package constraints");
@@ -614,7 +642,7 @@ export class Program implements IProgram {
614642
const maintainerAndPackageName =
615643
project.type === "package" ? project.maintainerAndPackageName : undefined;
616644

617-
this.connection.console.info(`Glob ${sourceDir}/**/*.elm`);
645+
this.host.logger.info(`Glob ${sourceDir}/**/*.elm`);
618646

619647
(await this.host.readDirectory(sourceDir)).forEach((matchingPath) => {
620648
matchingPath = normalizeUri(matchingPath);
@@ -668,7 +696,7 @@ export class Program implements IProgram {
668696
callback: () => void,
669697
): Promise<void> {
670698
try {
671-
this.connection.console.info(`Adding ${filePath.path.toString()}`);
699+
this.host.logger.info(`Adding ${filePath.path.toString()}`);
672700
const fileContent: string = await this.host.readFile(
673701
filePath.path.toString(),
674702
);
@@ -685,7 +713,7 @@ export class Program implements IProgram {
685713
);
686714
callback();
687715
} catch (error) {
688-
this.connection.console.error(error.stack);
716+
this.host.logger.error(error.stack);
689717
}
690718
}
691719

@@ -716,7 +744,7 @@ export class Program implements IProgram {
716744
}
717745
}
718746

719-
export function createNodeProgramHost(): IProgramHost {
747+
export function createNodeProgramHost(connection?: Connection): IProgramHost {
720748
return {
721749
readFile: (uri): Promise<string> =>
722750
readFile(uri, {
@@ -730,5 +758,22 @@ export function createNodeProgramHost(): IProgramHost {
730758
watchFile: (uri: string, callback: () => void): void => {
731759
chokidar.watch(uri).on("change", callback);
732760
},
761+
logger: connection?.console ?? {
762+
info: (): void => {
763+
//
764+
},
765+
warn: (): void => {
766+
//
767+
},
768+
error: (): void => {
769+
//
770+
},
771+
},
772+
handleError: (): void => {
773+
//
774+
},
775+
onServerDidRestart: (): void => {
776+
//
777+
},
733778
};
734779
}

src/compiler/typeChecker.ts

+7-6
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,7 @@ import {
1010
EUnionVariant,
1111
EPortAnnotation,
1212
} from "./utils/expressionTree";
13-
import { IProgram } from "./program";
14-
import { container } from "tsyringe";
15-
import { Connection } from "vscode-languageserver";
13+
import { IProgram, IProgramHost } from "./program";
1614
import {
1715
Type,
1816
TUnknown,
@@ -33,6 +31,7 @@ import { isKernelProject, nameIsKernel } from "./utils/elmUtils";
3331
import { existsSync } from "fs";
3432
import * as path from "../util/path";
3533
import { URI } from "vscode-uri";
34+
import { hostname } from "os";
3635

3736
export let bindTime = 0;
3837
export function resetBindTime(): void {
@@ -97,7 +96,10 @@ export interface TypeChecker {
9796
) => SyntaxNode[];
9897
}
9998

100-
export function createTypeChecker(program: IProgram): TypeChecker {
99+
export function createTypeChecker(
100+
program: IProgram,
101+
host: IProgramHost,
102+
): TypeChecker {
101103
const forest = program.getForest();
102104
const imports = new Map<string, Imports>();
103105

@@ -242,8 +244,7 @@ export function createTypeChecker(program: IProgram): TypeChecker {
242244

243245
return TUnknown;
244246
} catch (error) {
245-
const connection = container.resolve<Connection>("Connection");
246-
connection.console.warn(`Error while trying to infer a type. ${error}`);
247+
host.logger.warn(`Error while trying to infer a type. ${error}`);
247248
return TUnknown;
248249
}
249250
}

0 commit comments

Comments
 (0)