Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Standalone compiler API #568

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
152 changes: 97 additions & 55 deletions src/compiler/program.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import fs from "fs";
import globby from "globby";
import os from "os";
import "reflect-metadata";
import { container } from "tsyringe";
import util from "util";
import { Connection } from "vscode-languageserver";
import { URI } from "vscode-uri";
import Parser, { Tree } from "web-tree-sitter";
import { ICancellationToken } from "../cancellation";
Expand All @@ -17,7 +17,7 @@ import {
IPossibleImportsCache,
PossibleImportsCache,
} from "../util/possibleImportsCache";
import { Settings } from "../util/settings";
import { getDefaultSettings, IClientSettings } from "../util/settings";
import { Diagnostic } from "./diagnostics";
import { TypeCache } from "./typeCache";
import {
Expand All @@ -26,7 +26,8 @@ import {
TypeChecker,
} from "./typeChecker";
import chokidar from "chokidar";
import { CommandManager } from "../commandManager";
import { Connection } from "vscode-languageserver";
import { loadParser } from "../parser";

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

Expand Down Expand Up @@ -87,6 +88,7 @@ export interface IProgram {
sourceFile: ISourceFile,
importableModuleName: string,
): ISourceFile | undefined;
getSourceFiles(): ISourceFile[];
getForest(synchronize?: boolean): IForest;
getRootPath(): URI;
getTypeCache(): TypeCache;
Expand Down Expand Up @@ -135,16 +137,34 @@ interface IElmPackage extends IElmProject {
exposedModules: Set<string>;
}

export async function createProgram(
rootPath: URI,
programHost?: IProgramHost,
settings?: IClientSettings,
progressCallback?: (percent: number) => void,
): Promise<IProgram> {
const program = new Program(rootPath, programHost, settings);
await program.init(progressCallback);
return program;
}

export interface IProgramHost {
readFile(uri: string): Promise<string>;
readDirectory(uri: string): Promise<string[]>;
watchFile(uri: string, callback: () => void): void;
logger: {
info(message: string): void;
warn(message: string): void;
error(message: string): void;
};
handleError(message: string): void;
onServerDidRestart(
handler: (progressReporter: (progress: number) => void) => Promise<void>,
): void;
}

export class Program implements IProgram {
private parser: Parser;
private connection: Connection;
private settings: Settings;
private parser!: Parser;
private typeCache: TypeCache;
private typeChecker: TypeChecker | undefined;
private dirty = true;
Expand All @@ -157,24 +177,27 @@ export class Program implements IProgram {
private resolvedPackageCache = new Map<string, IElmPackage>();
private host: IProgramHost;
private filesWatching = new Set<string>();
private settings: IClientSettings;

constructor(private rootPath: URI, programHost?: IProgramHost) {
this.settings = container.resolve("Settings");
this.connection = container.resolve("Connection");
this.parser = container.resolve("Parser");
this.connection.console.info(
`Starting language server for folder: ${this.rootPath.toString()}`,
);

constructor(
private rootPath: URI,
programHost?: IProgramHost,
settings?: IClientSettings,
) {
this.typeCache = new TypeCache();
this.possibleImportsCache = new PossibleImportsCache();
this.operatorsCache = new Map<string, DefinitionResult>();
this.diagnosticsCache = new Map<string, Diagnostic[]>();
this.host = programHost ?? createNodeProgramHost();
this.settings = settings ?? getDefaultSettings();

this.host.logger.info(
`Starting language server for folder: ${this.rootPath.toString()}`,
);
}

public async init(
progressCallback: (percent: number) => void,
progressCallback?: (percent: number) => void,
): Promise<void> {
await this.initWorkspace(progressCallback);
}
Expand Down Expand Up @@ -215,6 +238,10 @@ export class Program implements IProgram {
}
}

public getSourceFiles(): ISourceFile[] {
return Array.from(this.getForest().treeMap.values());
}

public getForest(synchronize = true): IForest {
if (this.dirty && synchronize) {
this.forest.synchronize();
Expand All @@ -238,7 +265,10 @@ export class Program implements IProgram {
this.dirty = false;
}

return this.typeChecker ?? (this.typeChecker = createTypeChecker(this));
return (
this.typeChecker ??
(this.typeChecker = createTypeChecker(this, this.host))
);
}

public markAsDirty(): void {
Expand Down Expand Up @@ -323,41 +353,34 @@ export class Program implements IProgram {
}

private async initWorkspace(
progressCallback: (percent: number) => void,
progressCallback?: (percent: number) => void,
): Promise<void> {
const clientSettings = await this.settings.getClientSettings();
if (!container.isRegistered("Parser")) {
await loadParser(this.host);
}

this.parser = container.resolve("Parser");

let progress = 0;
let elmVersion;
try {
elmVersion = utils.getElmVersion(
clientSettings,
this.rootPath,
this.connection,
);
elmVersion = utils.getElmVersion(this.settings, this.rootPath, this.host);
} catch (error) {
this.connection.console.warn(
this.host.logger.warn(
`Could not figure out elm version, this will impact how good the server works. \n ${error.stack}`,
);
}

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

if (!this.filesWatching.has(pathToElmJson)) {
this.host.watchFile(pathToElmJson, () => {
void this.connection.window
.createWorkDoneProgress()
.then((progress) => {
progress.begin("Restarting Elm Language Server", 0);

this.initWorkspace((percent: number) => {
progress.report(percent, `${percent.toFixed(0)}%`);
})
.then(() => progress.done())
.catch(() => {
//
});
this.host.onServerDidRestart(async (progressReporter) => {
await this.initWorkspace((percent: number) => {
progressReporter(percent);
});
});
});
this.filesWatching.add(pathToElmJson);
}
Expand All @@ -371,11 +394,11 @@ export class Program implements IProgram {
// Run `elm make` to download dependencies
try {
utils.execCmdSync(
clientSettings.elmPath,
this.settings.elmPath,
"elm",
{ cmdArguments: ["make"] },
this.rootPath.fsPath,
this.connection,
this.host,
);
} catch (error) {
// On application projects, this will give a NO INPUT error message, but will still download the dependencies
Expand All @@ -386,38 +409,38 @@ export class Program implements IProgram {
this.forest = new Forest(this.rootProject);

const elmFilePaths = await this.findElmFilesInProject(this.rootProject);
this.connection.console.info(
this.host.logger.info(
`Found ${elmFilePaths.length.toString()} files to add to the project`,
);

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

const promiseList: Promise<void>[] = [];
const PARSE_STAGES = 3;
const PARSE_STAGES = 2;
const progressDelta = 100 / (elmFilePaths.length * PARSE_STAGES);
for (const filePath of elmFilePaths) {
progressCallback((progress += progressDelta));
if (progressCallback) {
progressCallback((progress += progressDelta));
}
promiseList.push(
this.readAndAddToForest(filePath, () => {
progressCallback((progress += progressDelta));
if (progressCallback) {
progressCallback((progress += progressDelta));
}
}),
);
}
await Promise.all(promiseList);

this.findExposedModulesOfDependencies(this.rootProject);

CommandManager.initHandlers(this.connection);

this.connection.console.info(
`Done parsing all files for ${pathToElmJson}`,
);
this.host.logger.info(`Done parsing all files for ${pathToElmJson}`);
} catch (error) {
this.connection.console.error(
this.host.logger.error(
`Error parsing files for ${pathToElmJson}:\n${error.stack}`,
);
}
Expand Down Expand Up @@ -467,7 +490,7 @@ export class Program implements IProgram {
);

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

this.connection.console.info(`Glob ${sourceDir}/**/*.elm`);
this.host.logger.info(`Glob ${sourceDir}/**/*.elm`);

(await this.host.readDirectory(sourceDir)).forEach((matchingPath) => {
matchingPath = normalizeUri(matchingPath);
Expand Down Expand Up @@ -668,7 +691,7 @@ export class Program implements IProgram {
callback: () => void,
): Promise<void> {
try {
this.connection.console.info(`Adding ${filePath.path.toString()}`);
this.host.logger.info(`Adding ${filePath.path.toString()}`);
const fileContent: string = await this.host.readFile(
filePath.path.toString(),
);
Expand All @@ -685,7 +708,7 @@ export class Program implements IProgram {
);
callback();
} catch (error) {
this.connection.console.error(error.stack);
this.host.logger.error(error.stack);
}
}

Expand Down Expand Up @@ -716,7 +739,7 @@ export class Program implements IProgram {
}
}

export function createNodeProgramHost(): IProgramHost {
export function createNodeProgramHost(connection?: Connection): IProgramHost {
return {
readFile: (uri): Promise<string> =>
readFile(uri, {
Expand All @@ -730,5 +753,24 @@ export function createNodeProgramHost(): IProgramHost {
watchFile: (uri: string, callback: () => void): void => {
chokidar.watch(uri).on("change", callback);
},
logger: connection?.console ?? {
info: (): void => {
//
},
warn: (): void => {
//
},
error: (): void => {
//
},
},
handleError:
connection?.window.showErrorMessage.bind(createNodeProgramHost) ??
((): void => {
//
}),
onServerDidRestart: (): void => {
//
},
};
}
12 changes: 6 additions & 6 deletions src/compiler/typeChecker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,7 @@ import {
EUnionVariant,
EPortAnnotation,
} from "./utils/expressionTree";
import { IProgram } from "./program";
import { container } from "tsyringe";
import { Connection } from "vscode-languageserver";
import { IProgram, IProgramHost } from "./program";
import {
Type,
TUnknown,
Expand Down Expand Up @@ -97,7 +95,10 @@ export interface TypeChecker {
) => SyntaxNode[];
}

export function createTypeChecker(program: IProgram): TypeChecker {
export function createTypeChecker(
program: IProgram,
host: IProgramHost,
): TypeChecker {
const forest = program.getForest();
const imports = new Map<string, Imports>();

Expand Down Expand Up @@ -242,8 +243,7 @@ export function createTypeChecker(program: IProgram): TypeChecker {

return TUnknown;
} catch (error) {
const connection = container.resolve<Connection>("Connection");
connection.console.warn(`Error while trying to infer a type. ${error}`);
host.logger.warn(`Error while trying to infer a type. ${error}`);
return TUnknown;
}
}
Expand Down
Loading