From 50b21325fc67e24d4ee61974a4df07ef68450a9b Mon Sep 17 00:00:00 2001 From: Jon Bockhorst Date: Tue, 30 Mar 2021 19:33:08 -0500 Subject: [PATCH] WIP compiler API --- src/compiler/program.ts | 152 +++++++++++------- src/compiler/typeChecker.ts | 12 +- src/compiler/utils/elmUtils.ts | 16 +- src/index.ts | 27 +--- src/module.ts | 2 + src/parser.ts | 14 ++ .../codeAction/installPackageCodeAction.ts | 8 +- .../codeAction/moveFunctionCodeAction.ts | 2 +- src/providers/codeActionProvider.ts | 2 +- src/providers/codeLensProvider.ts | 2 +- .../diagnostics/elmMakeDiagnostics.ts | 5 +- .../diagnostics/elmReviewDiagnostics.ts | 5 +- src/providers/documentFormatingProvider.ts | 7 +- src/server.ts | 101 +++++++----- src/util/settings.ts | 9 +- test/standalone.ts | 25 +++ 16 files changed, 241 insertions(+), 148 deletions(-) create mode 100644 src/parser.ts create mode 100644 test/standalone.ts diff --git a/src/compiler/program.ts b/src/compiler/program.ts index 68c04ac8b..3d2f7cf86 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -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"; @@ -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 { @@ -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); @@ -87,6 +88,7 @@ export interface IProgram { sourceFile: ISourceFile, importableModuleName: string, ): ISourceFile | undefined; + getSourceFiles(): ISourceFile[]; getForest(synchronize?: boolean): IForest; getRootPath(): URI; getTypeCache(): TypeCache; @@ -135,16 +137,34 @@ interface IElmPackage extends IElmProject { exposedModules: Set; } +export async function createProgram( + rootPath: URI, + programHost?: IProgramHost, + settings?: IClientSettings, + progressCallback?: (percent: number) => void, +): Promise { + const program = new Program(rootPath, programHost, settings); + await program.init(progressCallback); + return program; +} + export interface IProgramHost { readFile(uri: string): Promise; readDirectory(uri: string): Promise; 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; } 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; @@ -157,24 +177,27 @@ export class Program implements IProgram { private resolvedPackageCache = new Map(); private host: IProgramHost; private filesWatching = new Set(); + 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(); this.diagnosticsCache = new Map(); 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 { await this.initWorkspace(progressCallback); } @@ -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(); @@ -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 { @@ -323,41 +353,34 @@ export class Program implements IProgram { } private async initWorkspace( - progressCallback: (percent: number) => void, + progressCallback?: (percent: number) => void, ): Promise { - 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); } @@ -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 @@ -386,24 +409,28 @@ 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[] = []; - 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)); + } }), ); } @@ -411,13 +438,9 @@ export class Program implements IProgram { 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}`, ); } @@ -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"); @@ -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); @@ -668,7 +691,7 @@ export class Program implements IProgram { callback: () => void, ): Promise { 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(), ); @@ -685,7 +708,7 @@ export class Program implements IProgram { ); callback(); } catch (error) { - this.connection.console.error(error.stack); + this.host.logger.error(error.stack); } } @@ -716,7 +739,7 @@ export class Program implements IProgram { } } -export function createNodeProgramHost(): IProgramHost { +export function createNodeProgramHost(connection?: Connection): IProgramHost { return { readFile: (uri): Promise => readFile(uri, { @@ -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 => { + // + }, }; } diff --git a/src/compiler/typeChecker.ts b/src/compiler/typeChecker.ts index c48effc18..f5bcf88df 100644 --- a/src/compiler/typeChecker.ts +++ b/src/compiler/typeChecker.ts @@ -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, @@ -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(); @@ -242,8 +243,7 @@ export function createTypeChecker(program: IProgram): TypeChecker { return TUnknown; } catch (error) { - const connection = container.resolve("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; } } diff --git a/src/compiler/utils/elmUtils.ts b/src/compiler/utils/elmUtils.ts index 8e050e486..8d7d7f23c 100644 --- a/src/compiler/utils/elmUtils.ts +++ b/src/compiler/utils/elmUtils.ts @@ -4,7 +4,7 @@ import { Connection, CompletionItemKind } from "vscode-languageserver"; import { URI } from "vscode-uri"; import { IElmPackageCache } from "../elmPackageCache"; import { IClientSettings } from "../../util/settings"; -import { ElmProject } from "../program"; +import { ElmProject, IProgramHost } from "../program"; export const isWindows = process.platform === "win32"; @@ -25,7 +25,7 @@ export function execCmdSync( cmdStatic: string, options: IExecCmdOptions = {}, cwd: string, - connection: Connection, + host: IProgramHost, input?: string, ): ExecaSyncReturnValue { const cmd = cmdFromUser === "" ? cmdStatic : cmdFromUser; @@ -41,9 +41,9 @@ export function execCmdSync( stripFinalNewline: false, }); } catch (error) { - connection.console.warn(JSON.stringify(error)); - if (error.code && error.code === "ENOENT") { - connection.window.showErrorMessage( + host.logger.warn(JSON.stringify(error)); + if (error.errno && error.errno === "ENOENT") { + host.handleError( options.notFoundText ? options.notFoundText + ` I'm looking for '${cmd}' at '${cwd}'` : `Cannot find executable with name '${cmd}'`, @@ -81,7 +81,7 @@ export function getEmptyTypes(): { export function getElmVersion( settings: IClientSettings, elmWorkspaceFolder: URI, - connection: Connection, + host: IProgramHost, ): string { const options = { cmdArguments: ["--version"], @@ -94,12 +94,12 @@ export function getElmVersion( "elm", options, elmWorkspaceFolder.fsPath, - connection, + host, ); const version = result.stdout.trim(); - connection.console.info(`Elm version ${version} detected.`); + host.logger.info(`Elm version ${version} detected.`); return version; } diff --git a/src/index.ts b/src/index.ts index 1e4a82e3a..cf1148f24 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,5 @@ #!/usr/bin/env node -import * as Path from "path"; import "reflect-metadata"; import { container } from "tsyringe"; //must be after reflect-metadata import { @@ -10,9 +9,10 @@ import { ProposedFeatures, } from "vscode-languageserver"; import { createConnection } from "vscode-languageserver/node"; -import Parser from "web-tree-sitter"; import { getCancellationStrategyFromArgv } from "./cancellation"; import { CapabilityCalculator } from "./capabilityCalculator"; +import { createNodeProgramHost } from "./compiler/program"; +import { loadParser } from "./parser"; import { ASTProvider } from "./providers"; import { ElmAnalyseJsonService, @@ -41,7 +41,6 @@ container.register("Connection", { cancellationStrategy: getCancellationStrategyFromArgv(process.argv), }), }); -container.registerSingleton("Parser", Parser); container.registerSingleton("DocumentEvents", DocumentEvents); container.registerSingleton( @@ -62,25 +61,7 @@ connection.onInitialize( cancel, progress, ): Promise => { - await Parser.init(); - const absolute = Path.join(__dirname, "tree-sitter-elm.wasm"); - const pathToWasm = Path.relative(process.cwd(), absolute); - connection.console.info( - `Loading Elm tree-sitter syntax from ${pathToWasm}`, - ); - const language = await Parser.Language.load(pathToWasm); - const parser = container.resolve("Parser"); - // const logger: Parser.Logger = ( - // message: string, - // params: { [param: string]: string }, - // isLexMessage: "lex" | "parse", - // ) => { - // let type = isLexMessage ? "lex" : "parse"; - // if (type === "lex") type += " "; - // connection.console.info(`${type} ${message}`); - // }; - // parser.setLogger(logger); - parser.setLanguage(language); + await loadParser(createNodeProgramHost(connection)); container.register(CapabilityCalculator, { useValue: new CapabilityCalculator(params.capabilities), @@ -88,7 +69,7 @@ connection.onInitialize( const initializationOptions = params.initializationOptions ?? {}; - container.register("Settings", { + container.register(Settings, { useValue: new Settings(initializationOptions, params.capabilities), }); diff --git a/src/module.ts b/src/module.ts index 5a917cc25..ab946b8c8 100644 --- a/src/module.ts +++ b/src/module.ts @@ -1 +1,3 @@ export * as Protocol from "./protocol"; +export { Program, IProgram, IProgramHost } from "./compiler/program"; +export { URI } from "vscode-uri"; diff --git a/src/parser.ts b/src/parser.ts new file mode 100644 index 000000000..fa1255367 --- /dev/null +++ b/src/parser.ts @@ -0,0 +1,14 @@ +import { container } from "tsyringe"; +import Parser from "web-tree-sitter"; +import { IProgramHost } from "./compiler/program"; +import * as path from "path"; + +export async function loadParser(host: IProgramHost): Promise { + container.registerSingleton("Parser", Parser); + await Parser.init(); + const absolute = path.join(__dirname, "..", "tree-sitter-elm.wasm"); + const pathToWasm = path.relative(process.cwd(), absolute); + host.logger.info(`Loading Elm tree-sitter syntax from ${pathToWasm}`); + const language = await Parser.Language.load(pathToWasm); + container.resolve("Parser").setLanguage(language); +} diff --git a/src/providers/codeAction/installPackageCodeAction.ts b/src/providers/codeAction/installPackageCodeAction.ts index ea51eb822..438c84a0c 100644 --- a/src/providers/codeAction/installPackageCodeAction.ts +++ b/src/providers/codeAction/installPackageCodeAction.ts @@ -12,6 +12,7 @@ import { Diagnostics } from "../../compiler/diagnostics"; import { CodeActionProvider } from "../codeActionProvider"; import { ICodeActionParams } from "../paramsExtensions"; import { comparePackageRanking } from "../ranking"; +import { createNodeProgramHost, IProgramHost } from "../../compiler/program"; const errorCodes = [Diagnostics.ImportMissing.code]; const fixId = "install_package"; @@ -51,8 +52,9 @@ CodeActionProvider.registerCodeAction({ CommandManager.register( commandName, async (uri: string, packageName: string) => { - const settings = container.resolve("Settings"); + const settings = container.resolve(Settings); const connection = container.resolve("Connection"); + const host = createNodeProgramHost(connection); const program = new ElmWorkspaceMatcher((uri: string) => URI.parse(uri), @@ -66,7 +68,7 @@ CommandManager.register( "elm", { cmdArguments: ["install", packageName] }, program.getRootPath().fsPath, - connection, + host, clientSettings.skipInstallPackageConfirmation ? "y\n" : undefined, ); } catch (e) { @@ -91,7 +93,7 @@ CommandManager.register( "elm", { cmdArguments: ["install", packageName] }, program.getRootPath().fsPath, - connection, + host, `${choice.value}\n`, ); diff --git a/src/providers/codeAction/moveFunctionCodeAction.ts b/src/providers/codeAction/moveFunctionCodeAction.ts index 18b5483b7..4ce300ca5 100644 --- a/src/providers/codeAction/moveFunctionCodeAction.ts +++ b/src/providers/codeAction/moveFunctionCodeAction.ts @@ -13,7 +13,7 @@ const refactorName = "move_function"; CodeActionProvider.registerRefactorAction(refactorName, { getAvailableActions: (params: ICodeActionParams): IRefactorCodeAction[] => { if ( - !container.resolve("Settings").extendedCapabilities + !container.resolve(Settings).extendedCapabilities ?.moveFunctionRefactoringSupport ) { return []; diff --git a/src/providers/codeActionProvider.ts b/src/providers/codeActionProvider.ts index 99e33ead0..fffce389e 100644 --- a/src/providers/codeActionProvider.ts +++ b/src/providers/codeActionProvider.ts @@ -98,7 +98,7 @@ export class CodeActionProvider { private static preferredActions = new Map(); constructor() { - this.settings = container.resolve("Settings"); + this.settings = container.resolve(Settings); this.elmMake = container.resolve(ElmMakeDiagnostics); this.connection = container.resolve("Connection"); this.diagnosticsProvider = container.resolve(DiagnosticsProvider); diff --git a/src/providers/codeLensProvider.ts b/src/providers/codeLensProvider.ts index f6b8d4c0a..36afc3566 100644 --- a/src/providers/codeLensProvider.ts +++ b/src/providers/codeLensProvider.ts @@ -44,7 +44,7 @@ export class CodeLensProvider { constructor() { this.connection = container.resolve("Connection"); - this.settings = container.resolve("Settings"); + this.settings = container.resolve(Settings); this.connection.onCodeLens( new ElmWorkspaceMatcher((param: CodeLensParams) => URI.parse(param.textDocument.uri), diff --git a/src/providers/diagnostics/elmMakeDiagnostics.ts b/src/providers/diagnostics/elmMakeDiagnostics.ts index a63ec4e61..550596b17 100644 --- a/src/providers/diagnostics/elmMakeDiagnostics.ts +++ b/src/providers/diagnostics/elmMakeDiagnostics.ts @@ -19,6 +19,7 @@ import { IDiagnostic, IElmIssue } from "./diagnosticsProvider"; import { ElmDiagnosticsHelper } from "./elmDiagnosticsHelper"; import execa = require("execa"); import { ElmToolingJsonManager } from "../../elmToolingJsonManager"; +import { createNodeProgramHost } from "../../compiler/program"; const ELM_MAKE = "Elm"; export const NAMING_ERROR = "NAMING ERROR"; @@ -66,7 +67,7 @@ export class ElmMakeDiagnostics { private connection: Connection; constructor() { - this.settings = container.resolve("Settings"); + this.settings = container.resolve(Settings); this.connection = container.resolve("Connection"); this.elmToolingJsonManager = container.resolve( "ElmToolingJsonManager", @@ -240,7 +241,7 @@ export class ElmMakeDiagnostics { testOrMakeCommandWithOmittedSettings, options, workspaceRootPath, - this.connection, + createNodeProgramHost(this.connection), ); return []; } catch (error) { diff --git a/src/providers/diagnostics/elmReviewDiagnostics.ts b/src/providers/diagnostics/elmReviewDiagnostics.ts index dd9952a19..072020d34 100644 --- a/src/providers/diagnostics/elmReviewDiagnostics.ts +++ b/src/providers/diagnostics/elmReviewDiagnostics.ts @@ -16,6 +16,7 @@ import { Range } from "vscode-languageserver-textdocument"; import execa = require("execa"); import { existsSync } from "fs"; import * as path from "path"; +import { createNodeProgramHost } from "../../compiler/program"; export type IElmReviewDiagnostic = IDiagnostic & { data: { @@ -87,7 +88,7 @@ export class ElmReviewDiagnostics { private connection: Connection; constructor() { - this.settings = container.resolve("Settings"); + this.settings = container.resolve(Settings); this.connection = container.resolve("Connection"); this.elmWorkspaceMatcher = new ElmWorkspaceMatcher((uri) => uri); } @@ -142,7 +143,7 @@ export class ElmReviewDiagnostics { "elm-review", options, workspaceRootPath, - this.connection, + createNodeProgramHost(this.connection), ); return fileErrors; } catch (error) { diff --git a/src/providers/documentFormatingProvider.ts b/src/providers/documentFormatingProvider.ts index 61b6310aa..2be20a390 100644 --- a/src/providers/documentFormatingProvider.ts +++ b/src/providers/documentFormatingProvider.ts @@ -12,6 +12,7 @@ import { ElmWorkspaceMatcher } from "../util/elmWorkspaceMatcher"; import { Settings } from "../util/settings"; import { TextDocumentEvents } from "../util/textDocumentEvents"; import { IDocumentFormattingParams } from "./paramsExtensions"; +import { createNodeProgramHost } from "../compiler/program"; type DocumentFormattingResult = Promise; @@ -23,9 +24,9 @@ export class DocumentFormattingProvider { private diagnostics: DiagnosticsProvider; constructor() { - this.settings = container.resolve("Settings"); + this.settings = container.resolve(Settings); this.connection = container.resolve("Connection"); - this.events = container.resolve(TextDocumentEvents); + this.events = container.resolve(TextDocumentEvents); this.diagnostics = container.resolve(DiagnosticsProvider); this.connection.onDocumentFormatting( this.diagnostics.interruptDiagnostics(() => @@ -51,7 +52,7 @@ export class DocumentFormattingProvider { "elm-format", options, elmWorkspaceRootPath.fsPath, - this.connection, + createNodeProgramHost(this.connection), text, ); return Diff.getTextRangeChanges(text, format.stdout); diff --git a/src/server.ts b/src/server.ts index 53d5f2dfb..b5e39cf8c 100644 --- a/src/server.ts +++ b/src/server.ts @@ -11,10 +11,9 @@ import { URI, Utils } from "vscode-uri"; import { CapabilityCalculator } from "./capabilityCalculator"; import { ElmToolingJsonManager } from "./elmToolingJsonManager"; import { - Program, - IProgram, createNodeProgramHost, IProgramHost, + createProgram, } from "./compiler/program"; import { CodeActionProvider, @@ -39,6 +38,7 @@ import { Settings } from "./util/settings"; import { TextDocumentEvents } from "./util/textDocumentEvents"; import { FindTestsProvider } from "./providers/findTestsProvider"; import { ElmReviewDiagnostics } from "./providers/diagnostics/elmReviewDiagnostics"; +import { CommandManager } from "./commandManager"; export interface ILanguageServer { readonly capabilities: InitializeResult; @@ -79,30 +79,8 @@ export class Server implements ILanguageServer { `Found ${topLevelElmJsons.size} unique elmWorkspaces for workspace ${globUri}`, ); - const textDocuments = container.resolve(TextDocumentEvents); - - const nodeProgramHost = createNodeProgramHost(); - - // First try to read from text documents buffer, then fallback to disk - const programHost: IProgramHost = { - ...nodeProgramHost, - readFile: (uri) => { - const textDocument = textDocuments.get(URI.file(uri).toString()); - - if (textDocument) { - return Promise.resolve(textDocument.getText()); - } - - return nodeProgramHost.readFile(uri); - }, - }; - - const elmWorkspaces: Program[] = []; - topLevelElmJsons.forEach((elmWorkspace) => { - elmWorkspaces.push(new Program(elmWorkspace, programHost)); - }); - container.register("ElmWorkspaces", { - useValue: elmWorkspaces, + container.register("ElmWorkspacePaths", { + useValue: Array.from(topLevelElmJsons.values()), }); container.register("ElmToolingJsonManager", { useValue: new ElmToolingJsonManager(), @@ -127,29 +105,72 @@ export class Server implements ILanguageServer { } public async init(): Promise { + const textDocuments = container.resolve(TextDocumentEvents); + + const nodeProgramHost = createNodeProgramHost(); + + // First try to read from text documents buffer, then fallback to disk + const programHost: IProgramHost = { + ...nodeProgramHost, + readFile: (uri) => { + const textDocument = textDocuments.get(URI.file(uri).toString()); + + if (textDocument) { + return Promise.resolve(textDocument.getText()); + } + + return nodeProgramHost.readFile(uri); + }, + logger: this.connection.console, + handleError: this.connection.window.showErrorMessage.bind(this), + onServerDidRestart: async (handler) => { + const progress = await this.connection.window.createWorkDoneProgress(); + progress.begin("Restarting Elm Language Server", 0); + + await handler((percent) => { + progress.report(percent, `${percent.toFixed(0)}%`); + }); + + progress.done(); + }, + }; + + const settings = container.resolve(Settings); + const clientSettings = await settings.getClientSettings(); + this.progress.begin("Indexing Elm", 0); - const elmWorkspaces = container.resolve("ElmWorkspaces"); + const elmWorkspacePaths = container.resolve("ElmWorkspacePaths"); await Promise.all( - elmWorkspaces + elmWorkspacePaths .map((ws) => ({ ws, indexedPercent: 0 })) .map((indexingWs, _, all) => - indexingWs.ws.init((percent: number) => { - // update progress for this workspace - indexingWs.indexedPercent = percent; - - // report average progress across all workspaces - const avgIndexed = - all.reduce((sum, { indexedPercent }) => sum + indexedPercent, 0) / - all.length; - this.progress.report(avgIndexed, `${Math.round(avgIndexed)}%`); - }), + createProgram( + indexingWs.ws, + programHost, + clientSettings, + (percent: number) => { + // update progress for this workspace + indexingWs.indexedPercent = percent; + + // report average progress across all workspaces + const avgIndexed = + all.reduce( + (sum, { indexedPercent }) => sum + indexedPercent, + 0, + ) / all.length; + this.progress.report(avgIndexed, `${Math.round(avgIndexed)}%`); + }, + ), ), - ); + ).then((programs) => { + CommandManager.initHandlers(this.connection); + container.register("ElmWorkspaces", { useValue: programs }); + }); this.progress.done(); } public async registerInitializedProviders(): Promise { - const settings = container.resolve("Settings"); + const settings = container.resolve(Settings); // We can now query the client for up to date settings settings.initFinished(); diff --git a/src/util/settings.ts b/src/util/settings.ts index 6426b8d85..1d9f4c048 100644 --- a/src/util/settings.ts +++ b/src/util/settings.ts @@ -20,9 +20,8 @@ export interface IExtendedCapabilites { clientInitiatedDiagnostics: boolean; } -@injectable() -export class Settings { - private clientSettings: IClientSettings = { +export function getDefaultSettings(): IClientSettings { + return { elmFormatPath: "", elmPath: "", elmTestPath: "", @@ -33,6 +32,10 @@ export class Settings { onlyUpdateDiagnosticsOnSave: false, elmReviewDiagnostics: "off", }; +} +@injectable() +export class Settings { + private clientSettings: IClientSettings = getDefaultSettings(); private connection: Connection; private initDone = false; diff --git a/test/standalone.ts b/test/standalone.ts new file mode 100644 index 000000000..7fe694a47 --- /dev/null +++ b/test/standalone.ts @@ -0,0 +1,25 @@ +import path from "path"; +import { URI } from "vscode-uri"; +import { createNodeProgramHost, createProgram } from "../src/compiler/program"; + +async function run(): Promise { + const host = createNodeProgramHost(); + host.logger = console; + + const rootUri = URI.parse(path.join(__dirname, "../../../elm-engage-common")); + const program = await createProgram(rootUri, host); + + const checker = program.getTypeChecker(); + program.getSourceFiles().forEach((sourceFile) => { + if (sourceFile.writeable) { + sourceFile.tree.rootNode.children.forEach((node) => { + if (node.type === "value_declaration") { + const type = checker.findType(node); + console.log(checker.typeToString(type)); + } + }); + } + }); +} + +void run();