diff --git a/package-lock.json b/package-lock.json index c9cb3349..e886792c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7629,25 +7629,25 @@ } }, "vscode-jsonrpc": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-5.0.1.tgz", - "integrity": "sha512-JvONPptw3GAQGXlVV2utDcHx0BiY34FupW/kI6mZ5x06ER5DdPG/tXWMVHjTNULF5uKPOUUD0SaXg5QaubJL0A==" + "version": "6.0.0-next.7", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-6.0.0-next.7.tgz", + "integrity": "sha512-1nG+6cuTtpzmXe7yYfO9GCkYlyV6Ai+jDnwidHiT2T7zhc+bJM+VTtc0T/CdTlDyTNTqIcCj0V1nD4TcVjJ7Ug==" }, "vscode-languageserver": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-6.1.1.tgz", - "integrity": "sha512-DueEpkUAkD5XTR4MLYNr6bQIp/UFR0/IPApgXU3YfCBCB08u2sm9hRCs6DxYZELkk++STPjpcjksR2H8qI3cDQ==", + "version": "7.0.0-next.11", + "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-7.0.0-next.11.tgz", + "integrity": "sha512-FVJ9se5fJsMvoPKRLlVFaNv6+RJ+EWP4vYkF8J+eR2CPbbQ6CzN1wLq3lqZTXBVOBh1toJR5B6z4j+j+OffFNA==", "requires": { - "vscode-languageserver-protocol": "^3.15.3" + "vscode-languageserver-protocol": "3.16.0-next.11" } }, "vscode-languageserver-protocol": { - "version": "3.15.3", - "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.15.3.tgz", - "integrity": "sha512-zrMuwHOAQRhjDSnflWdJG+O2ztMWss8GqUUB8dXLR/FPenwkiBNkMIJJYfSN6sgskvsF0rHAoBowNQfbyZnnvw==", + "version": "3.16.0-next.11", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.16.0-next.11.tgz", + "integrity": "sha512-31FmupmSmfznuMuGp7qN6h3d/hKUbexbvcwTvrUE/igqRlzFU542s8MtGICx1ERbVuDOLGp96W2Z92qbUbmBPA==", "requires": { - "vscode-jsonrpc": "^5.0.1", - "vscode-languageserver-types": "3.15.1" + "vscode-jsonrpc": "6.0.0-next.7", + "vscode-languageserver-types": "3.16.0-next.5" } }, "vscode-languageserver-textdocument": { @@ -7656,9 +7656,9 @@ "integrity": "sha512-UIcJDjX7IFkck7cSkNNyzIz5FyvpQfY7sdzVy+wkKN/BLaD4DQ0ppXQrKePomCxTS7RrolK1I0pey0bG9eh8dA==" }, "vscode-languageserver-types": { - "version": "3.15.1", - "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.15.1.tgz", - "integrity": "sha512-+a9MPUQrNGRrGU630OGbYVQ+11iOIovjCkqxajPa9w57Sd5ruK8WQNsslzpa0x/QJqC8kRc2DUxWjIFwoNm4ZQ==" + "version": "3.16.0-next.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.16.0-next.5.tgz", + "integrity": "sha512-lf8Y1XXMtF1r2oDDAmJe+drizNXkybSRXAQQk5dPy2rYJsY9SPXYNO074L3THu9zNYepzV5fRJZUPo/V/TLBRQ==" }, "vscode-uri": { "version": "2.1.2", diff --git a/package.json b/package.json index 282484c9..67f9ae16 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "version": "1.13.2", "author": "Kolja Lampe", "license": "MIT", + "main": "./out/module.js", "files": [ "out" ], @@ -24,7 +25,7 @@ "reflect-metadata": "^0.1.13", "ts-debounce": "^2.0.1", "tsyringe": "^4.3.0", - "vscode-languageserver": "^6.1.1", + "vscode-languageserver": "^7.0.0-next.11", "vscode-languageserver-textdocument": "1.0.1", "vscode-uri": "^2.1.2", "web-tree-sitter": "^0.17.1" diff --git a/src/cancellation.ts b/src/cancellation.ts new file mode 100644 index 00000000..8ff0e8a5 --- /dev/null +++ b/src/cancellation.ts @@ -0,0 +1,221 @@ +/* eslint-disable @typescript-eslint/no-use-before-define */ +import * as fs from "fs"; +import * as os from "os"; +import path from "path"; +import { performance } from "perf_hooks"; +import { + AbstractCancellationTokenSource, + CancellationId, + CancellationReceiverStrategy, + CancellationSenderStrategy, + CancellationStrategy, + CancellationToken, + Emitter, + Event, +} from "vscode-languageserver"; + +/** + * File based cancellation mostly taken from pyright: https://github.com/microsoft/pyright/blob/a9d2528574087cc2f8c10a7c3aaeb287eb64a870/packages/pyright-internal/src/common/cancellationUtils.ts#L48 + */ + +class FileBasedToken implements CancellationToken { + private _isCancelled = false; + private _emitter: Emitter | undefined; + + constructor(private _cancellationFilePath: string) {} + + public cancel(): void { + if (!this._isCancelled) { + this._isCancelled = true; + if (this._emitter) { + this._emitter.fire(undefined); + this.dispose(); + } + } + } + + get isCancellationRequested(): boolean { + if (this._isCancelled) { + return true; + } + + if (this._pipeExists()) { + // the first time it encounters cancellation file, it will + // cancel itself and raise cancellation event. + // in this mode, cancel() might not be called explicitly by jsonrpc layer + this.cancel(); + } + + return this._isCancelled; + } + + get onCancellationRequested(): Event { + if (!this._emitter) { + this._emitter = new Emitter(); + } + return this._emitter.event; + } + + public dispose(): void { + if (this._emitter) { + this._emitter.dispose(); + this._emitter = undefined; + } + } + + private _pipeExists(): boolean { + try { + fs.statSync(this._cancellationFilePath); + return true; + } catch (e) { + return false; + } + } +} + +export class FileBasedCancellationTokenSource + implements AbstractCancellationTokenSource { + private _token: CancellationToken | undefined; + constructor(private _cancellationFilePath: string) {} + + get token(): CancellationToken { + if (!this._token) { + // be lazy and create the token only when + // actually needed + this._token = new FileBasedToken(this._cancellationFilePath); + } + return this._token; + } + + cancel(): void { + if (!this._token) { + // save an object by returning the default + // cancelled token when cancellation happens + // before someone asks for the token + this._token = CancellationToken.Cancelled; + } else { + (this._token as FileBasedToken).cancel(); + } + } + + dispose(): void { + if (!this._token) { + // ensure to initialize with an empty token if we had none + this._token = CancellationToken.None; + } else if (this._token instanceof FileBasedToken) { + // actually dispose + this._token.dispose(); + } + } +} + +export function getCancellationFolderPath(folderName: string): string { + return path.join(os.tmpdir(), "elm-language-server-cancellation", folderName); +} + +export function getCancellationFilePath( + folderName: string, + id: CancellationId, +): string { + return path.join( + getCancellationFolderPath(folderName), + `cancellation-${String(id)}.tmp`, + ); +} + +class FileCancellationReceiverStrategy implements CancellationReceiverStrategy { + constructor(readonly folderName: string) {} + + createCancellationTokenSource( + id: CancellationId, + ): AbstractCancellationTokenSource { + return new FileBasedCancellationTokenSource( + getCancellationFilePath(this.folderName, id), + ); + } +} + +let cancellationFolderName: string; + +export function getCancellationStrategyFromArgv( + argv: string[], +): CancellationStrategy { + let receiver: CancellationReceiverStrategy | undefined; + + for (let i = 0; i < argv.length; i++) { + const arg = argv[i]; + if (arg === "--cancellationReceive") { + receiver = createReceiverStrategyFromArgv(argv[i + 1]); + } else { + const args = arg.split("="); + if (args[0] === "--cancellationReceive") { + receiver = createReceiverStrategyFromArgv(args[1]); + } + } + } + + if (receiver && !cancellationFolderName) { + cancellationFolderName = (receiver as FileCancellationReceiverStrategy) + .folderName; + } + + receiver = receiver ? receiver : CancellationReceiverStrategy.Message; + return { receiver, sender: CancellationSenderStrategy.Message }; + + function createReceiverStrategyFromArgv( + arg: string, + ): CancellationReceiverStrategy | undefined { + const folderName = extractCancellationFolderName(arg); + return folderName + ? new FileCancellationReceiverStrategy(folderName) + : undefined; + } + + function extractCancellationFolderName(arg: string): string | undefined { + const fileRegex = /^file:(.+)$/; + const folderName = fileRegex.exec(arg); + return folderName ? folderName[1] : undefined; + } +} + +export class OperationCanceledException {} + +export interface ICancellationToken { + isCancellationRequested(): boolean; + + /** @throws OperationCanceledException if isCancellationRequested is true */ + throwIfCancellationRequested(): void; +} + +/** + * ThrottledCancellationToken taken from Typescript: https://github.com/microsoft/TypeScript/blob/79ffd03f8b73010fa03cef624e5f1770bc9c975b/src/services/services.ts#L1152 + */ +export class ThrottledCancellationToken implements ICancellationToken { + // Store when we last tried to cancel. Checking cancellation can be expensive (as we have + // to marshall over to the host layer). So we only bother actually checking once enough + // time has passed. + private lastCancellationCheckTime = 0; + + constructor( + private cancellationToken: CancellationToken, + private readonly throttleWaitMilliseconds = 20, + ) {} + + public isCancellationRequested(): boolean { + const time = performance.now(); + const duration = Math.abs(time - this.lastCancellationCheckTime); + if (duration >= this.throttleWaitMilliseconds) { + // Check no more than once every throttle wait milliseconds + this.lastCancellationCheckTime = time; + return this.cancellationToken.isCancellationRequested; + } + + return false; + } + + public throwIfCancellationRequested(): void { + if (this.isCancellationRequested()) { + throw new OperationCanceledException(); + } + } +} diff --git a/src/capabilityCalculator.ts b/src/capabilityCalculator.ts index 71e42890..416cbc23 100644 --- a/src/capabilityCalculator.ts +++ b/src/capabilityCalculator.ts @@ -3,7 +3,6 @@ import { ServerCapabilities, TextDocumentSyncKind, } from "vscode-languageserver"; -import * as ElmAnalyseDiagnostics from "./providers/diagnostics/elmAnalyseDiagnostics"; import * as ElmMakeDiagnostics from "./providers/diagnostics/elmMakeDiagnostics"; export class CapabilityCalculator { @@ -28,11 +27,7 @@ export class CapabilityCalculator { documentFormattingProvider: true, documentSymbolProvider: true, executeCommandProvider: { - commands: [ - ElmAnalyseDiagnostics.CODE_ACTION_ELM_ANALYSE, - ElmAnalyseDiagnostics.CODE_ACTION_ELM_ANALYSE_FIX_ALL, - ElmMakeDiagnostics.CODE_ACTION_ELM_MAKE, - ], + commands: [ElmMakeDiagnostics.CODE_ACTION_ELM_MAKE], }, foldingRangeProvider: true, hoverProvider: true, diff --git a/src/elmWorkspace.ts b/src/elmWorkspace.ts index 807ffe54..2a64de0a 100644 --- a/src/elmWorkspace.ts +++ b/src/elmWorkspace.ts @@ -4,7 +4,7 @@ import os from "os"; import path from "path"; import { container } from "tsyringe"; import util from "util"; -import { IConnection } from "vscode-languageserver"; +import { Connection } from "vscode-languageserver"; import { URI } from "vscode-uri"; import Parser, { Tree } from "web-tree-sitter"; import { Forest, IForest } from "./forest"; @@ -55,7 +55,7 @@ export class ElmWorkspace implements IElmWorkspace { private elmFolders: IRootFolder[] = []; private forest: IForest = new Forest([]); private parser: Parser; - private connection: IConnection; + private connection: Connection; private settings: Settings; private typeCache: TypeCache; private typeChecker: TypeChecker | undefined; diff --git a/src/index.ts b/src/index.ts index dd963385..e9ae1d7c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,13 +4,14 @@ import * as Path from "path"; import "reflect-metadata"; import { container } from "tsyringe"; //must be after reflect-metadata import { - createConnection, - IConnection, + Connection, InitializeParams, InitializeResult, 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 { ILanguageServer } from "./server"; import { DocumentEvents } from "./util/documentEvents"; @@ -30,8 +31,10 @@ if (process.argv.length === 2) { } // Composition root - be aware, there are some register calls that need to be done later -container.register("Connection", { - useValue: createConnection(ProposedFeatures.all), +container.register("Connection", { + useValue: createConnection(ProposedFeatures.all, { + cancellationStrategy: getCancellationStrategyFromArgv(process.argv), + }), }); container.registerSingleton("Parser", Parser); @@ -40,7 +43,7 @@ container.register(TextDocumentEvents, { useValue: new TextDocumentEvents(), }); -const connection = container.resolve("Connection"); +const connection = container.resolve("Connection"); let server: ILanguageServer; @@ -62,6 +65,7 @@ connection.onInitialize( container.register(CapabilityCalculator, { useValue: new CapabilityCalculator(params.capabilities), }); + const initializationOptions = params.initializationOptions ?? {}; container.register("Settings", { diff --git a/src/module.ts b/src/module.ts new file mode 100644 index 00000000..5a917cc2 --- /dev/null +++ b/src/module.ts @@ -0,0 +1 @@ +export * as Protocol from "./protocol"; diff --git a/src/protocol.ts b/src/protocol.ts index cfef38e4..c997596d 100644 --- a/src/protocol.ts +++ b/src/protocol.ts @@ -1,16 +1,14 @@ import { CodeActionParams, RequestType } from "vscode-languageserver"; import { URI } from "vscode-uri"; +// eslint-disable-next-line @typescript-eslint/no-namespace export const GetMoveDestinationRequest = new RequestType< IMoveParams, IMoveDestinationsResponse, - void, void >("elm/getMoveDestinations"); -export const MoveRequest = new RequestType( - "elm/move", -); +export const MoveRequest = new RequestType("elm/move"); export interface IMoveParams { sourceUri: string; @@ -28,12 +26,9 @@ export interface IMoveDestination { uri: string; } -export const ExposeRequest = new RequestType< - IExposeUnexposeParams, - void, - void, - void ->("elm/expose"); +export const ExposeRequest = new RequestType( + "elm/expose", +); export interface IExposeUnexposeParams { uri: string; @@ -43,7 +38,6 @@ export interface IExposeUnexposeParams { export const UnexposeRequest = new RequestType< IExposeUnexposeParams, void, - void, void >("elm/unexpose"); @@ -58,13 +52,22 @@ export interface IOnDidRenameFilesParams { export const OnDidCreateFilesRequest = new RequestType< IOnDidCreateFilesParams, void, - void, void >("elm/ondidCreateFiles"); export const OnDidRenameFilesRequest = new RequestType< IOnDidRenameFilesParams, void, - void, void >("elm/ondidRenameFiles"); + +export interface IGetDiagnosticsParams { + files: string[]; + delay: number; +} + +export const GetDiagnosticsRequest = new RequestType< + IGetDiagnosticsParams, + void, + void +>("elm/getDiagnostics"); diff --git a/src/providers/astProvider.ts b/src/providers/astProvider.ts index 9ece40f5..4b212564 100644 --- a/src/providers/astProvider.ts +++ b/src/providers/astProvider.ts @@ -3,13 +3,13 @@ import { container } from "tsyringe"; import { DidChangeTextDocumentParams, DidOpenTextDocumentParams, - IConnection, VersionedTextDocumentIdentifier, Event, Emitter, + Connection, } from "vscode-languageserver"; import { URI } from "vscode-uri"; -import Parser, { Tree, Edit, Point } from "web-tree-sitter"; +import Parser, { Tree, Edit, Point, SyntaxNode } from "web-tree-sitter"; import { IElmWorkspace } from "../elmWorkspace"; import { ElmWorkspaceMatcher } from "../util/elmWorkspaceMatcher"; import { Position, Range } from "vscode-languageserver-textdocument"; @@ -19,20 +19,23 @@ import { TreeUtils } from "../util/treeUtils"; import { ITreeContainer } from "../forest"; export class ASTProvider { - private connection: IConnection; + private connection: Connection; private parser: Parser; private documentEvents: TextDocumentEvents; - private treeChangeEvent = new Emitter<{ treeContainer: ITreeContainer }>(); - readonly onTreeChange: Event<{ treeContainer: ITreeContainer }> = this - .treeChangeEvent.event; + private treeChangeEvent = new Emitter<{ + treeContainer: ITreeContainer; + declaration?: SyntaxNode; + }>(); + readonly onTreeChange: Event<{ + treeContainer: ITreeContainer; + declaration?: SyntaxNode; + }> = this.treeChangeEvent.event; constructor() { this.parser = container.resolve("Parser"); - this.connection = container.resolve("Connection"); - this.documentEvents = container.resolve( - TextDocumentEvents, - ); + this.connection = container.resolve("Connection"); + this.documentEvents = container.resolve(TextDocumentEvents); new FileEventsHandler(); @@ -77,6 +80,8 @@ export class ASTProvider { const newTree = this.parser.parse(newText, tree); + let changedDeclaration: SyntaxNode | undefined; + tree ?.getChangedRanges(newTree) .map((range) => [ @@ -98,6 +103,7 @@ export class ASTProvider { startNode.id === endNode.id && TreeUtils.getTypeAnnotation(startNode) ) { + changedDeclaration = startNode; elmWorkspace.getTypeCache().invalidateValueDeclaration(startNode); } else { elmWorkspace.getTypeCache().invalidateProject(); @@ -120,7 +126,10 @@ export class ASTProvider { setImmediate(() => { if (tree) { - this.treeChangeEvent.fire({ treeContainer }); + this.treeChangeEvent.fire({ + treeContainer, + declaration: changedDeclaration, + }); } }); } diff --git a/src/providers/codeActionProvider.ts b/src/providers/codeActionProvider.ts index 899094a9..34940abf 100644 --- a/src/providers/codeActionProvider.ts +++ b/src/providers/codeActionProvider.ts @@ -1,11 +1,9 @@ import { container } from "tsyringe"; import { - ApplyWorkspaceEditResponse, CodeAction, CodeActionKind, CodeActionParams, - ExecuteCommandParams, - IConnection, + Connection, TextEdit, } from "vscode-languageserver"; import { URI } from "vscode-uri"; @@ -15,7 +13,6 @@ import { ElmWorkspaceMatcher } from "../util/elmWorkspaceMatcher"; import { RefactorEditUtils } from "../util/refactorEditUtils"; import { IClientSettings, Settings } from "../util/settings"; import { TreeUtils } from "../util/treeUtils"; -import { ElmAnalyseDiagnostics } from "./diagnostics/elmAnalyseDiagnostics"; import { ElmLsDiagnostics } from "./diagnostics/elmLsDiagnostics"; import { ElmMakeDiagnostics } from "./diagnostics/elmMakeDiagnostics"; import { TypeInferenceDiagnostics } from "./diagnostics/typeInferenceDiagnostics"; @@ -23,9 +20,8 @@ import { ExposeUnexposeHandler } from "./handlers/exposeUnexposeHandler"; import { MoveRefactoringHandler } from "./handlers/moveRefactoringHandler"; export class CodeActionProvider { - private connection: IConnection; + private connection: Connection; private settings: Settings; - private elmAnalyse: ElmAnalyseDiagnostics | null = null; private elmMake: ElmMakeDiagnostics; private functionTypeAnnotationDiagnostics: TypeInferenceDiagnostics; private elmDiagnostics: ElmLsDiagnostics; @@ -34,26 +30,19 @@ export class CodeActionProvider { constructor() { this.settings = container.resolve("Settings"); this.clientSettings = container.resolve("ClientSettings"); - if (this.clientSettings.elmAnalyseTrigger !== "never") { - this.elmAnalyse = container.resolve( - ElmAnalyseDiagnostics, - ); - } - this.elmMake = container.resolve(ElmMakeDiagnostics); - this.functionTypeAnnotationDiagnostics = container.resolve< - TypeInferenceDiagnostics - >(TypeInferenceDiagnostics); - this.elmDiagnostics = container.resolve(ElmLsDiagnostics); - this.connection = container.resolve("Connection"); + this.elmMake = container.resolve(ElmMakeDiagnostics); + this.functionTypeAnnotationDiagnostics = container.resolve( + TypeInferenceDiagnostics, + ); + this.elmDiagnostics = container.resolve(ElmLsDiagnostics); + this.connection = container.resolve("Connection"); this.onCodeAction = this.onCodeAction.bind(this); - this.onExecuteCommand = this.onExecuteCommand.bind(this); this.connection.onCodeAction( new ElmWorkspaceMatcher((param: CodeActionParams) => URI.parse(param.textDocument.uri), ).handlerForWorkspace(this.onCodeAction.bind(this)), ); - this.connection.onExecuteCommand(this.onExecuteCommand.bind(this)); if (this.settings.extendedCapabilities?.moveFunctionRefactoringSupport) { new MoveRefactoringHandler(); @@ -67,8 +56,6 @@ export class CodeActionProvider { elmWorkspace: IElmWorkspace, ): CodeAction[] { this.connection.console.info("A code action was requested"); - const analyse = - (this.elmAnalyse && this.elmAnalyse.onCodeAction(params)) ?? []; const make = this.elmMake.onCodeAction(params); const typeAnnotation = this.functionTypeAnnotationDiagnostics.onCodeAction( params, @@ -77,20 +64,12 @@ export class CodeActionProvider { return [ ...this.getRefactorCodeActions(params, elmWorkspace), ...this.getTypeAnnotationCodeActions(params, elmWorkspace), - ...analyse, ...make, ...typeAnnotation, ...elmDiagnostics, ]; } - private async onExecuteCommand( - params: ExecuteCommandParams, - ): Promise { - this.connection.console.info("A command execution was requested"); - return this.elmAnalyse && this.elmAnalyse.onExecuteCommand(params); - } - private getTypeAnnotationCodeActions( params: CodeActionParams, elmWorkspace: IElmWorkspace, @@ -117,7 +96,7 @@ export class CodeActionProvider { !TreeUtils.getTypeAnnotation(nodeAtPosition.parent.parent) ) { const typeString: string = checker.typeToString( - checker.findType(nodeAtPosition.parent, params.textDocument.uri), + checker.findType(nodeAtPosition.parent), treeContainer, ); @@ -325,7 +304,7 @@ export class CodeActionProvider { ); const typeString: string = checker.typeToString( - checker.findType(nodeAtPosition, params.textDocument.uri), + checker.findType(nodeAtPosition), treeContainer, ); diff --git a/src/providers/codeLensProvider.ts b/src/providers/codeLensProvider.ts index d0b15a90..427b6712 100644 --- a/src/providers/codeLensProvider.ts +++ b/src/providers/codeLensProvider.ts @@ -3,7 +3,7 @@ import { CodeLens, CodeLensParams, Command, - IConnection, + Connection, Location, Position, Range, @@ -20,11 +20,11 @@ type CodeLensType = "exposed" | "referenceCounter"; type CodeLensResult = CodeLens[] | null | undefined; export class CodeLensProvider { - private readonly connection: IConnection; + private readonly connection: Connection; private readonly settings: Settings; constructor() { - this.connection = container.resolve("Connection"); + this.connection = container.resolve("Connection"); this.settings = container.resolve("Settings"); this.connection.onCodeLens( new ElmWorkspaceMatcher((param: CodeLensParams) => diff --git a/src/providers/completionProvider.ts b/src/providers/completionProvider.ts index 5edf3593..68fd0705 100644 --- a/src/providers/completionProvider.ts +++ b/src/providers/completionProvider.ts @@ -4,7 +4,7 @@ import { CompletionItemKind, CompletionList, CompletionParams, - IConnection, + Connection, InsertTextFormat, MarkupKind, Position, @@ -23,8 +23,9 @@ import { ImportUtils, IPossibleImport } from "../util/importUtils"; import { RefactorEditUtils } from "../util/refactorEditUtils"; import { TreeUtils } from "../util/treeUtils"; import RANKING_LIST from "./ranking"; +import { DiagnosticsProvider } from "."; +import { TypeChecker } from "../util/types/typeChecker"; import escapeStringRegexp from "escape-string-regexp"; -import { TypeChecker } from "src/util/types/typeChecker"; export type CompletionResult = | CompletionItem[] @@ -45,14 +46,18 @@ interface ICompletionOptions { export class CompletionProvider { private qidRegex = /[a-zA-Z0-9.]+/; - private connection: IConnection; + private connection: Connection; + private diagnostics: DiagnosticsProvider; constructor() { - this.connection = container.resolve("Connection"); - this.connection.onCompletion( - new ElmWorkspaceMatcher((param: CompletionParams) => - URI.parse(param.textDocument.uri), - ).handlerForWorkspace(this.handleCompletionRequest), + this.connection = container.resolve("Connection"); + this.diagnostics = container.resolve(DiagnosticsProvider); + this.connection.onCompletion((params) => + this.diagnostics.interuptDiagnostics(() => + new ElmWorkspaceMatcher((params: CompletionParams) => + URI.parse(params.textDocument.uri), + ).handlerForWorkspace(this.handleCompletionRequest)(params), + ), ); } @@ -342,10 +347,9 @@ export class CompletionProvider { } return this.getRecordCompletionsUsingInference( + checker, targetNode, replaceRange, - params.textDocument.uri, - elmWorkspace, ); } @@ -788,14 +792,12 @@ export class CompletionProvider { } private getRecordCompletionsUsingInference( + checker: TypeChecker, targetNode: SyntaxNode, replaceRange: Range, - uri: string, - elmWorkspace: IElmWorkspace, ): CompletionItem[] { const result = []; - const checker = elmWorkspace.getTypeChecker(); - const foundType = checker.findType(targetNode, uri); + const foundType = checker.findType(targetNode); if (foundType.nodeType === "Record") { for (const field in foundType.fields) { diff --git a/src/providers/definitionProvider.ts b/src/providers/definitionProvider.ts index 440fec08..a7f3464c 100644 --- a/src/providers/definitionProvider.ts +++ b/src/providers/definitionProvider.ts @@ -1,6 +1,6 @@ import { container } from "tsyringe"; import { - IConnection, + Connection, Location, LocationLink, Position, @@ -21,9 +21,9 @@ export type DefinitionResult = | undefined; export class DefinitionProvider { - private connection: IConnection; + private connection: Connection; constructor() { - this.connection = container.resolve("Connection"); + this.connection = container.resolve("Connection"); this.connection.onDefinition( new ElmWorkspaceMatcher((param: TextDocumentPositionParams) => URI.parse(param.textDocument.uri), diff --git a/src/providers/diagnostics/diagnosticsProvider.ts b/src/providers/diagnostics/diagnosticsProvider.ts index a1d9ce1a..f777a2bd 100644 --- a/src/providers/diagnostics/diagnosticsProvider.ts +++ b/src/providers/diagnostics/diagnosticsProvider.ts @@ -1,14 +1,19 @@ -import { IClientSettings, Settings } from "src/util/settings"; -import { debounce } from "ts-debounce"; import { container, injectable } from "tsyringe"; -import { Diagnostic, FileChangeType, IConnection } from "vscode-languageserver"; +import { + CancellationToken, + CancellationTokenSource, + Connection, + Diagnostic, + FileChangeType, +} from "vscode-languageserver"; import { TextDocument } from "vscode-languageserver-textdocument"; import { URI } from "vscode-uri"; -import { ElmAnalyseDiagnostics } from ".."; import { IElmWorkspace } from "../../elmWorkspace"; +import { GetDiagnosticsRequest } from "../../protocol"; +import { Delayer } from "../../util/delayer"; import { ElmWorkspaceMatcher } from "../../util/elmWorkspaceMatcher"; -import { NoWorkspaceContainsError } from "../../util/noWorkspaceContainsError"; -import { ElmAnalyseTrigger } from "../../util/settings"; +import { MultistepOperation } from "../../util/multistepOperation"; +import { IClientSettings } from "../../util/settings"; import { TextDocumentEvents } from "../../util/textDocumentEvents"; import { ASTProvider } from "../astProvider"; import { ElmLsDiagnostics } from "./elmLsDiagnostics"; @@ -31,56 +36,70 @@ export interface IElmIssue { file: string; } +class PendingDiagnostics extends Map { + public getOrderedFiles(): string[] { + return Array.from(this.entries()) + .sort((a, b) => a[1] - b[1]) + .map((a) => a[0]); + } +} + +interface IPendingRequest { + token: CancellationTokenSource; + files: string[]; +} + @injectable() export class DiagnosticsProvider { private elmMakeDiagnostics: ElmMakeDiagnostics; - private elmAnalyseDiagnostics: ElmAnalyseDiagnostics | null = null; private typeInferenceDiagnostics: TypeInferenceDiagnostics; - private elmDiagnostics: ElmLsDiagnostics; - private elmWorkspaceMatcher: ElmWorkspaceMatcher<{ uri: string }>; + private elmLsDiagnostics: ElmLsDiagnostics; private currentDiagnostics: Map; private events: TextDocumentEvents; - private connection: IConnection; - private settings: Settings; + private connection: Connection; private clientSettings: IClientSettings; private workspaces: IElmWorkspace[]; + private elmWorkspaceMatcher: ElmWorkspaceMatcher; + private documentEvents: TextDocumentEvents; + + private pendingRequest: IPendingRequest | undefined; + private pendingDiagnostics: PendingDiagnostics; + private diagnosticsDelayer: Delayer; + private diagnosticsOperation: MultistepOperation; + private changeSeq = 0; constructor() { - this.settings = container.resolve("Settings"); this.clientSettings = container.resolve("ClientSettings"); - if (this.clientSettings.elmAnalyseTrigger !== "never") { - this.elmAnalyseDiagnostics = container.resolve( - ElmAnalyseDiagnostics, - ); - } - this.elmMakeDiagnostics = container.resolve( - ElmMakeDiagnostics, - ); - this.typeInferenceDiagnostics = container.resolve( - TypeInferenceDiagnostics, - ); - this.elmDiagnostics = container.resolve(ElmLsDiagnostics); - this.connection = container.resolve("Connection"); - this.events = container.resolve(TextDocumentEvents); - this.elmWorkspaceMatcher = new ElmWorkspaceMatcher((doc) => - URI.parse(doc.uri), - ); + + this.elmMakeDiagnostics = container.resolve(ElmMakeDiagnostics); + this.typeInferenceDiagnostics = container.resolve(TypeInferenceDiagnostics); + this.elmLsDiagnostics = container.resolve(ElmLsDiagnostics); + this.documentEvents = container.resolve(TextDocumentEvents); + + this.connection = container.resolve("Connection"); + this.events = container.resolve(TextDocumentEvents); + this.elmWorkspaceMatcher = new ElmWorkspaceMatcher((uri) => uri); + this.diagnosticsOperation = new MultistepOperation(this.connection); + this.workspaces = container.resolve("ElmWorkspaces"); - const astProvider = container.resolve(ASTProvider); + const astProvider = container.resolve(ASTProvider); this.currentDiagnostics = new Map(); - // register onChange listener if settings are not on-save only + this.pendingDiagnostics = new PendingDiagnostics(); + this.diagnosticsDelayer = new Delayer(300); - const elmAnalyseTrigger = this.clientSettings.elmAnalyseTrigger; this.events.on( "open", - (d) => void this.getDiagnostics(d, true, elmAnalyseTrigger), + (d: { document: TextDocument }) => + void this.getElmMakeDiagnostics(d.document.uri), ); this.events.on( "save", - (d) => void this.getDiagnostics(d, true, elmAnalyseTrigger), + (d: { document: TextDocument }) => + void this.getElmMakeDiagnostics(d.document.uri), ); + this.connection.onDidChangeWatchedFiles((event) => { const newDeleteEvents = event.changes .filter((a) => a.type === FileChangeType.Deleted) @@ -89,16 +108,16 @@ export class DiagnosticsProvider { this.deleteDiagnostics(uri); }); }); - if (this.elmAnalyseDiagnostics) { - this.elmAnalyseDiagnostics.on( - "new-diagnostics", - this.newElmAnalyseDiagnostics.bind(this), - ); - } - if (elmAnalyseTrigger === "change") { - this.events.on( - "change", - (d) => void this.getDiagnostics(d, false, elmAnalyseTrigger), + + const clientInitiatedDiagnostics = + this.clientSettings.extendedCapabilities?.clientInitiatedDiagnostics ?? + false; + + if (clientInitiatedDiagnostics) { + this.connection.onRequest( + GetDiagnosticsRequest, + (params, cancellationToken) => + this.getDiagnostics(params.files, params.delay, cancellationToken), ); } @@ -116,9 +135,8 @@ export class DiagnosticsProvider { this.updateDiagnostics( treeContainer.uri, DiagnosticKind.ElmLS, - this.elmDiagnostics.createDiagnostics( - treeContainer.tree, - treeContainer.uri, + this.elmLsDiagnostics.createDiagnostics( + treeContainer, workspace, ), ); @@ -128,81 +146,91 @@ export class DiagnosticsProvider { } }); - this.workspaces.forEach((workspace) => { - workspace.getForest().treeMap.forEach((treeContainer) => { - if (treeContainer.writeable) { - const treeDiagnostics = this.typeInferenceDiagnostics.createDiagnostics( - treeContainer, - workspace, - ); - - this.updateDiagnostics( - treeContainer.uri, - DiagnosticKind.TypeInference, - treeDiagnostics, - ); + if (!clientInitiatedDiagnostics) { + this.requestAllDiagnostics(); + } - if (!this.clientSettings.disableElmLSDiagnostics) { - this.updateDiagnostics( - treeContainer.uri, - DiagnosticKind.ElmLS, - this.elmDiagnostics.createDiagnostics( - treeContainer.tree, - treeContainer.uri, - workspace, - ), - ); - } + astProvider.onTreeChange(({ treeContainer, declaration }) => { + if (!clientInitiatedDiagnostics) { + if (this.pendingRequest) { + this.pendingRequest.token.cancel(); } - }); - astProvider.onTreeChange(({ treeContainer }) => { - let workspace; - try { - workspace = this.elmWorkspaceMatcher.getElmWorkspaceFor({ - uri: treeContainer.uri, - }); - } catch (error) { - if (error instanceof NoWorkspaceContainsError) { - this.connection.console.info(error.message); - return; // ignore file that doesn't correspond to a workspace - } + this.requestDiagnostics(treeContainer.uri); + } + }); - throw error; - } + this.documentEvents.on("change", () => { + this.change(); + this.typeInferenceDiagnostics.change(); + }); + } - this.updateDiagnostics( - treeContainer.uri, - DiagnosticKind.TypeInference, - this.typeInferenceDiagnostics.createDiagnostics( - treeContainer, - workspace, - ), - ); + private requestDiagnostics(uri: string): void { + this.pendingDiagnostics.set(uri, Date.now()); + this.triggerDiagnostics(); + } - if (!this.clientSettings.disableElmLSDiagnostics) { - this.updateDiagnostics( - treeContainer.uri, - DiagnosticKind.ElmLS, - this.elmDiagnostics.createDiagnostics( - treeContainer.tree, - treeContainer.uri, - workspace, - ), - ); + private requestAllDiagnostics(): void { + this.workspaces.forEach((workspace) => { + workspace.getForest().treeMap.forEach(({ uri, writeable }) => { + if (writeable) { + this.pendingDiagnostics.set(uri, Date.now()); } }); }); + + this.triggerDiagnostics(); } - private newElmAnalyseDiagnostics( - diagnostics: Map, - ): void { - this.resetDiagnostics(diagnostics, DiagnosticKind.ElmAnalyse); + public interuptDiagnostics(f: () => T): T { + if (!this.pendingRequest) { + return f(); + } - diagnostics.forEach((diagnostics, uri) => { - this.updateDiagnostics(uri, DiagnosticKind.ElmAnalyse, diagnostics); - }); + this.pendingRequest.token.cancel(); + this.pendingRequest = undefined; + const result = f(); + + this.triggerDiagnostics(); + return result; + } + + private triggerDiagnostics(delay = 200): void { + const sendPendingDiagnostics = (): void => { + const orderedFiles = this.pendingDiagnostics.getOrderedFiles(); + + if (this.pendingRequest) { + this.pendingRequest.token.cancel(); + + this.pendingRequest.files.forEach((file) => { + if (!orderedFiles.includes(file)) { + orderedFiles.push(file); + } + }); + + this.pendingRequest = undefined; + } + + // Add all open files to request + const openFiles = this.events.getManagedUris(); + openFiles.forEach((file) => { + if (!orderedFiles.includes(file)) { + orderedFiles.push(file); + } + }); + + if (orderedFiles.length) { + this.pendingRequest = { + token: this.getDiagnosticsWithCancellation(orderedFiles, 0), + files: orderedFiles, + }; + } + + this.pendingDiagnostics.clear(); + }; + + void this.diagnosticsDelayer.trigger(sendPendingDiagnostics, delay); } private updateDiagnostics( @@ -224,17 +252,11 @@ export class DiagnosticsProvider { } if (didUpdate) { - const sendDiagnostics = (uri: string): void => { - const fileDiagnostics = this.currentDiagnostics.get(uri); - this.connection.sendDiagnostics({ - uri, - diagnostics: fileDiagnostics ? fileDiagnostics.get() : [], - }); - }; - - const sendDiagnosticsDebounced = debounce(sendDiagnostics, 50); - - sendDiagnosticsDebounced(uri); + const fileDiagnostics = this.currentDiagnostics.get(uri); + this.connection.sendDiagnostics({ + uri, + diagnostics: fileDiagnostics ? fileDiagnostics.get() : [], + }); } } @@ -246,51 +268,108 @@ export class DiagnosticsProvider { }); } - private async getDiagnostics( - { document }: { document: TextDocument }, - isSaveOrOpen: boolean, - elmAnalyseTrigger: ElmAnalyseTrigger, - ): Promise { - this.connection.console.info( - `Diagnostics were requested due to a file ${ - isSaveOrOpen ? "open or save" : "change" - }`, - ); + private getDiagnosticsWithCancellation( + files: string[], + delay: number, + ): CancellationTokenSource { + const cancellationToken = new CancellationTokenSource(); - const uri = URI.parse(document.uri); + this.getDiagnostics(files, delay, cancellationToken.token); - const text = document.getText(); + return cancellationToken; + } - if (isSaveOrOpen) { - const elmMakeDiagnostics = await this.elmMakeDiagnostics.createDiagnostics( - uri, - ); + private getDiagnostics( + files: string[], + delay: number, + cancellationToken: CancellationToken, + ): void { + const followMs = Math.min(delay, 200); + + this.diagnosticsOperation.startNew( + cancellationToken, + (next) => { + const seq = this.changeSeq; + + let index = 0; + const goNext = (): void => { + index++; + if (files.length > index) { + next.delay(followMs, checkOne); + } + }; + + const checkOne = async (): Promise => { + if (this.changeSeq !== seq) { + return; + } - this.resetDiagnostics(elmMakeDiagnostics, DiagnosticKind.ElmMake); + const uri = files[index]; + const workspace = this.elmWorkspaceMatcher.getElmWorkspaceFor( + URI.parse(uri), + ); - elmMakeDiagnostics.forEach((diagnostics, diagnosticsUri) => { - this.updateDiagnostics( - diagnosticsUri, - DiagnosticKind.ElmMake, - diagnostics, - ); - }); - } + const treeContainer = workspace.getForest().getByUri(uri); - const elmMakeDiagnosticsForCurrentFile = - this.currentDiagnostics - .get(uri.toString()) - ?.getForKind(DiagnosticKind.ElmMake) ?? []; - - if ( - this.elmAnalyseDiagnostics && - elmAnalyseTrigger !== "never" && - (!elmMakeDiagnosticsForCurrentFile || - (elmMakeDiagnosticsForCurrentFile && - elmMakeDiagnosticsForCurrentFile.length === 0)) - ) { - await this.elmAnalyseDiagnostics.updateFile(uri, text); - } + if (!treeContainer) { + goNext(); + return; + } + + this.updateDiagnostics( + uri, + DiagnosticKind.TypeInference, + await this.typeInferenceDiagnostics.getDiagnosticsForFile( + treeContainer, + workspace, + cancellationToken, + ), + ); + + if (this.changeSeq !== seq) { + return; + } + + next.immediate(() => { + this.updateDiagnostics( + uri, + DiagnosticKind.ElmLS, + this.elmLsDiagnostics.createDiagnostics(treeContainer, workspace), + ); + goNext(); + }); + }; + + if (files.length > 0 && this.changeSeq === seq) { + next.delay(delay, checkOne); + } + }, + () => { + // + }, + ); + } + + public async getElmMakeDiagnostics(uri: string): Promise { + const elmMakeDiagnostics = await this.elmMakeDiagnostics.createDiagnostics( + URI.parse(uri), + ); + + this.resetDiagnostics(elmMakeDiagnostics, DiagnosticKind.ElmMake); + + elmMakeDiagnostics.forEach((diagnostics, diagnosticsUri) => { + this.updateDiagnostics( + diagnosticsUri, + DiagnosticKind.ElmMake, + diagnostics, + ); + }); + + this.currentDiagnostics.forEach((_, uri) => { + if (!elmMakeDiagnostics.has(uri)) { + this.updateDiagnostics(uri, DiagnosticKind.ElmMake, []); + } + }); } private resetDiagnostics( @@ -306,4 +385,8 @@ export class DiagnosticsProvider { } }); } + + private change(): void { + this.changeSeq++; + } } diff --git a/src/providers/diagnostics/elmAnalyseDiagnostics.ts b/src/providers/diagnostics/elmAnalyseDiagnostics.ts deleted file mode 100644 index 6de676ad..00000000 --- a/src/providers/diagnostics/elmAnalyseDiagnostics.ts +++ /dev/null @@ -1,418 +0,0 @@ -import { randomBytes } from "crypto"; -import { ElmApp, FixedFile, Message, Report } from "elm-analyse/ts/domain"; -import { EventEmitter } from "events"; -import * as fs from "fs"; -import * as path from "path"; -import { container, injectable } from "tsyringe"; -import util from "util"; -import { - ApplyWorkspaceEditResponse, - CodeAction, - CodeActionKind, - CodeActionParams, - Diagnostic, - DiagnosticSeverity, - DiagnosticTag, - ExecuteCommandParams, - IConnection, - TextEdit, -} from "vscode-languageserver"; -import { TextDocument } from "vscode-languageserver-textdocument"; -import { URI } from "vscode-uri"; -import { IElmWorkspace } from "../../elmWorkspace"; -import * as Diff from "../../util/diff"; -import { ElmWorkspaceMatcher } from "../../util/elmWorkspaceMatcher"; -import { Settings } from "../../util/settings"; -import { TextDocumentEvents } from "../../util/textDocumentEvents"; -import { DocumentFormattingProvider } from "../documentFormatingProvider"; - -const readFile = util.promisify(fs.readFile); -const fixableErrors = [ - "UnnecessaryParens", - "UnusedImport", - "UnusedImportedVariable", - "UnusedImportAlias", - "UnusedPatternVariable", - "UnusedTypeAlias", - "MultiLineRecordFormatting", - "DropConsOfItemAndList", - "DuplicateImport", -]; -const ELM_ANALYSE = "elm-analyse"; -const RANDOM_ID = randomBytes(16).toString("hex"); -export const CODE_ACTION_ELM_ANALYSE = `elmLS.elmAnalyseFixer-${RANDOM_ID}`; -export const CODE_ACTION_ELM_ANALYSE_FIX_ALL = `elmLS.elmAnalyseFixer.fixAll-${RANDOM_ID}`; - -export interface IElmAnalyseEvents { - on(event: "new-report", diagnostics: Map): this; -} - -@injectable() -export class ElmAnalyseDiagnostics { - private elmAnalysers: Map>; - private diagnostics: Map; - private filesWithDiagnostics: Set = new Set(); - private eventEmitter: EventEmitter = new EventEmitter(); - private elmWorkspaceMatcher: ElmWorkspaceMatcher; - private events: TextDocumentEvents; - private connection: IConnection; - private settings: Settings; - private formattingProvider: DocumentFormattingProvider; - - constructor() { - const elmWorkspaces = container.resolve("ElmWorkspaces"); - this.formattingProvider = container.resolve(DocumentFormattingProvider); - this.settings = container.resolve("Settings"); - this.connection = container.resolve("Connection"); - this.events = container.resolve(TextDocumentEvents); - this.onExecuteCommand = this.onExecuteCommand.bind(this); - this.onCodeAction = this.onCodeAction.bind(this); - this.diagnostics = new Map(); - this.elmWorkspaceMatcher = new ElmWorkspaceMatcher((uri) => uri); - - this.elmAnalysers = new Map( - elmWorkspaces.map((ws) => [ws, this.setupElmAnalyse(ws)]), - ); - } - - public on( - event: string | symbol, - listener: (diagnostics: Map) => void, - ): this { - this.eventEmitter.on(event, listener); - return this; - } - - public async updateFile(uri: URI, text?: string): Promise { - const workspace = this.elmWorkspaceMatcher.getElmWorkspaceFor(uri); - const analyser = this.elmAnalysers.get(workspace); - if (!analyser) { - throw new Error( - `No elm-analyse instance loaded for workspace ${uri.fsPath}.`, - ); - } - - await analyser.then((elmAnalyser) => { - elmAnalyser.ports.fileWatch.send({ - content: text ?? null, - event: "update", - file: path.relative(workspace.getRootPath().fsPath, uri.fsPath), - }); - }); - } - - public onCodeAction(params: CodeActionParams): CodeAction[] { - const { uri } = params.textDocument; - - // The `CodeActionParams` will only have diagnostics for the region we were in, for the - // "Fix All" feature we need to know about all of the fixable things in the document - const fixableDiagnostics = this.fixableDiagnostics( - this.diagnostics.get(uri.toString()) ?? [], - ); - - const fixAllInFile: CodeAction[] = - fixableDiagnostics.length > 1 - ? [ - { - command: { - arguments: [uri], - command: CODE_ACTION_ELM_ANALYSE_FIX_ALL, - title: `Fix all ${fixableDiagnostics.length} issues`, - }, - diagnostics: fixableDiagnostics, - kind: CodeActionKind.QuickFix, - title: `Fix all ${fixableDiagnostics.length} issues`, - }, - ] - : []; - - const contextDiagnostics: CodeAction[] = this.fixableDiagnostics( - params.context.diagnostics, - ).map((diagnostic) => { - const title = diagnostic.message.split("\n")[0]; - return { - command: { - arguments: [uri, diagnostic], - command: CODE_ACTION_ELM_ANALYSE, - title, - }, - diagnostics: [diagnostic], - kind: CodeActionKind.QuickFix, - title, - }; - }); - - return contextDiagnostics.length > 0 - ? contextDiagnostics.concat(fixAllInFile) - : []; - } - - public async onExecuteCommand( - params: ExecuteCommandParams, - ): Promise { - let uri: URI; - switch (params.command) { - case CODE_ACTION_ELM_ANALYSE: { - if (!params.arguments || params.arguments.length !== 2) { - this.connection.console.warn( - "Received incorrect number of arguments for elm-analyse fixer. Returning early.", - ); - return; - } - uri = params.arguments[0]; - const diagnostic: Diagnostic = params.arguments[1]; - const code: number = - typeof diagnostic.code === "number" ? diagnostic.code : -1; - - if (code === -1) { - this.connection.console.warn( - "Unable to apply elm-analyse fix, unknown diagnostic code", - ); - return; - } - return this.fixer(uri, code); - } - case CODE_ACTION_ELM_ANALYSE_FIX_ALL: { - if (!params.arguments || params.arguments.length !== 1) { - this.connection.console.warn( - "Received incorrect number of arguments for elm-analyse fixer. Returning early.", - ); - return; - } - uri = params.arguments[0]; - return this.fixer(uri); - } - } - } - - private fixableDiagnostics(diagnostics: Diagnostic[]): Diagnostic[] { - return diagnostics.filter( - (diagnostic) => - diagnostic.source === ELM_ANALYSE && this.isFixable(diagnostic), - ); - } - - /** - * If a diagnosticId is provided it will fix the single issue, if no - * id is provided it will fix the entire file. - */ - private async fixer( - uri: URI, - diagnosticId?: number, - ): Promise { - const elmWorkspace = this.elmWorkspaceMatcher.getElmWorkspaceFor(uri); - - const edits = await this.getFixEdits(elmWorkspace, uri, diagnosticId); - - return this.connection.workspace.applyEdit({ - changes: { - [uri.toString()]: edits, - }, - }); - } - - private async getFixEdits( - elmWorkspace: IElmWorkspace, - uri: URI, - code?: number, - ): Promise { - const elmAnalyse = await this.elmAnalysers.get(elmWorkspace); - const settings = await this.settings.getClientSettings(); - - if (!elmAnalyse) { - throw new Error( - `No elm-analyse instance loaded for workspace ${uri.fsPath}.`, - ); - } - - const filePath = URI.parse(uri.toString()).fsPath; - const relativePath = path.relative( - elmWorkspace.getRootPath().fsPath, - filePath, - ); - - return new Promise((resolve, reject) => { - // Naming the function here so that we can unsubscribe once we get the new file content - const onFixComplete = ( - fixedFile: FixedFile, - ): Promise | undefined | void => { - this.connection.console.info( - `Received fixed file from elm-analyse for path: ${filePath}`, - ); - elmAnalyse.ports.sendFixedFile.unsubscribe(onFixComplete); - const oldText = this.events.get(uri.toString()); - if (!oldText) { - return reject( - "Unable to apply elm-analyse fix, file content was unavailable.", - ); - } - - // This formats the fixed file with elm-format first and then figures out the - // diffs from there, this prevents needing to chain sets of edits - resolve( - this.formattingProvider - .formatText( - elmWorkspace.getRootPath(), - settings.elmFormatPath, - fixedFile.content, - ) - .then( - async (elmFormatEdits) => - await this.createEdits( - oldText.getText(), - fixedFile.content, - elmFormatEdits, - ), - ), - ); - }; - - elmAnalyse.ports.sendFixedFile.subscribe(onFixComplete); - - if (typeof code === "number") { - this.connection.console.info( - `Sending elm-analyse fix request for diagnostic id: ${code}`, - ); - elmAnalyse.ports.onFixQuick.send(code); - } else { - this.connection.console.info( - `Sending elm-analyse fix request for file: ${relativePath}`, - ); - elmAnalyse.ports.onFixFileQuick.send(relativePath); - } - }); - } - - private async createEdits( - oldText: string, - newText: string, - elmFormatEdits: TextEdit[] | undefined, - ): Promise { - if (elmFormatEdits) { - // Fake a `TextDocument` so that we can use `applyEdits` on `TextDocument` - const formattedFile = TextDocument.create( - "file://fakefile.elm", - "elm", - 0, - newText, - ); - return Diff.getTextRangeChanges( - oldText, - TextDocument.applyEdits(formattedFile, elmFormatEdits), - ); - } else { - return Diff.getTextRangeChanges(oldText, newText); - } - } - - private async setupElmAnalyse(elmWorkspace: IElmWorkspace): Promise { - const fsPath = elmWorkspace.getRootPath().fsPath; - const elmJson = await readFile(path.join(fsPath, "elm.json"), { - encoding: "utf-8", - }).then(JSON.parse); - const fileLoadingPorts = require("elm-analyse/dist/app/file-loading-ports.js"); - const { Elm } = require("elm-analyse/dist/app/backend-elm.js"); - const elmAnalyse = Elm.Analyser.init({ - flags: { - project: elmJson, - registry: [], - server: false, - }, - }); - - // elm-analyse breaks if there is a trailing slash on the path, it tries to - // read //elm.json instead of
/elm.json - fileLoadingPorts.setup(elmAnalyse, {}, fsPath.replace(/[\\/]?$/, "")); - - return new Promise((resolve) => { - // Wait for elm-analyse to send back the first report - const cb = (firstReport: Report): void => { - elmAnalyse.ports.sendReportValue.unsubscribe(cb); - const onNewReport = this.onNewReportForWorkspace(elmWorkspace); - onNewReport(firstReport); - elmAnalyse.ports.sendReportValue.subscribe(onNewReport); - resolve(elmAnalyse); - }; - elmAnalyse.ports.sendReportValue.subscribe(cb); - }); - } - - private onNewReportForWorkspace = (elmWorkspace: IElmWorkspace) => ( - report: Report, - ): void => { - this.connection.console.info( - `Received new elm-analyse report with ${report.messages.length} messages`, - ); - - // When publishing diagnostics it looks like you have to publish - // for one URI at a time, so this groups all of the messages for - // each file and sends them as a batch - this.diagnostics = report.messages.reduce((acc, message) => { - const uri = URI.file( - path.join(elmWorkspace.getRootPath().fsPath, message.file), - ).toString(); - const arr = acc.get(uri) ?? []; - arr.push(this.messageToDiagnostic(message)); - acc.set(uri, arr); - return acc; - }, new Map()); - const filesInReport = new Set(this.diagnostics.keys()); - const filesThatAreNowFixed = new Set( - [...this.filesWithDiagnostics].filter( - (uriPath) => !filesInReport.has(uriPath), - ), - ); - - this.filesWithDiagnostics = filesInReport; - - // When you fix the last error in a file it no longer shows up in the report, but - // we still need to clear the error marker for it - filesThatAreNowFixed.forEach((file) => this.diagnostics.set(file, [])); - this.eventEmitter.emit("new-diagnostics", this.diagnostics); - }; - - private isFixable(diagnostic: Diagnostic): boolean { - return fixableErrors.some((e) => diagnostic.message.includes(e)); - } - - private messageToDiagnostic(message: Message): Diagnostic { - if (message.type === "FileLoadFailed") { - return { - code: "1", - message: "Error parsing file", - range: { - end: { line: 1, character: 0 }, - start: { line: 0, character: 0 }, - }, - severity: DiagnosticSeverity.Error, - source: ELM_ANALYSE, - }; - } - - const rangeDefaults = [1, 1, 2, 1]; - const [lineStart, colStart, lineEnd, colEnd] = - (message.data && - message.data.properties && - message.data.properties.range) ?? - rangeDefaults; - - const range = { - end: { line: lineEnd - 1, character: colEnd - 1 }, - start: { line: lineStart - 1, character: colStart - 1 }, - }; - return { - code: message.id, - // Clean up the error message a bit, removing the end of the line, e.g. - // "Record has only one field. Use the field's type or introduce a Type. At ((14,5),(14,20))" - message: `${ - message.data.description.split(/at .+$/i)[0] - }\nSee https://stil4m.github.io/elm-analyse/#/messages/${message.type}`, - range, - severity: DiagnosticSeverity.Warning, - source: ELM_ANALYSE, - tags: message.data.description.startsWith("Unused ") - ? [DiagnosticTag.Unnecessary] - : undefined, - }; - } -} diff --git a/src/providers/diagnostics/elmLsDiagnostics.ts b/src/providers/diagnostics/elmLsDiagnostics.ts index 91782d0a..a32dad7a 100644 --- a/src/providers/diagnostics/elmLsDiagnostics.ts +++ b/src/providers/diagnostics/elmLsDiagnostics.ts @@ -1,4 +1,5 @@ /* eslint-disable @typescript-eslint/no-unsafe-call */ +import { ITreeContainer } from "../../forest"; import { container } from "tsyringe"; import { CodeAction, @@ -328,10 +329,11 @@ export class ElmLsDiagnostics { } public createDiagnostics = ( - tree: Tree, - uri: string, + treeContainer: ITreeContainer, elmWorkspace: IElmWorkspace, ): Diagnostic[] => { + const tree = treeContainer.tree; + const uri = treeContainer.uri; try { return [ ...this.getUnusedImportDiagnostics(tree), @@ -806,9 +808,7 @@ export class ElmLsDiagnostics { recordTypes.forEach((recordType) => { let isSingleField = true; if (recordType.parent?.type === "type_ref" && recordType.parent.parent) { - const type = elmWorkspace - .getTypeChecker() - .findType(recordType.parent, uri); + const type = elmWorkspace.getTypeChecker().findType(recordType.parent); const singleField = recordType.descendantsOfType( "lower_case_identifier", diff --git a/src/providers/diagnostics/elmMakeDiagnostics.ts b/src/providers/diagnostics/elmMakeDiagnostics.ts index b60c5a85..4ed7e5c4 100644 --- a/src/providers/diagnostics/elmMakeDiagnostics.ts +++ b/src/providers/diagnostics/elmMakeDiagnostics.ts @@ -9,7 +9,7 @@ import { CodeActionKind, CodeActionParams, Diagnostic, - IConnection, + Connection, TextEdit, } from "vscode-languageserver"; import { URI } from "vscode-uri"; @@ -25,7 +25,7 @@ import { Utils } from "../../util/utils"; import { IElmIssue } from "./diagnosticsProvider"; import { ElmDiagnosticsHelper } from "./elmDiagnosticsHelper"; import execa = require("execa"); -import { IElmWorkspace } from "src/elmWorkspace"; +import { IElmWorkspace } from "../../elmWorkspace"; const ELM_MAKE = "Elm"; const NAMING_ERROR = "NAMING ERROR"; @@ -112,11 +112,11 @@ export class ElmMakeDiagnostics { { moduleName: string; valueName?: string; diagnostic: Diagnostic }[] >(); private settings: Settings; - private connection: IConnection; + private connection: Connection; constructor() { this.settings = container.resolve("Settings"); - this.connection = container.resolve("Connection"); + this.connection = container.resolve("Connection"); this.elmWorkspaceMatcher = new ElmWorkspaceMatcher((uri) => uri); } diff --git a/src/providers/diagnostics/fileDiagnostics.ts b/src/providers/diagnostics/fileDiagnostics.ts index bbebc78d..2a2b72ce 100644 --- a/src/providers/diagnostics/fileDiagnostics.ts +++ b/src/providers/diagnostics/fileDiagnostics.ts @@ -46,7 +46,6 @@ export class FileDiagnostics { public get(): Diagnostic[] { return [ ...this.getForKind(DiagnosticKind.ElmMake), - ...this.getForKind(DiagnosticKind.ElmAnalyse), ...this.getForKind(DiagnosticKind.ElmTest), ...this.getForKind(DiagnosticKind.TypeInference), ...this.getForKind(DiagnosticKind.ElmLS), diff --git a/src/providers/diagnostics/typeInferenceDiagnostics.ts b/src/providers/diagnostics/typeInferenceDiagnostics.ts index f388712c..c302abe1 100644 --- a/src/providers/diagnostics/typeInferenceDiagnostics.ts +++ b/src/providers/diagnostics/typeInferenceDiagnostics.ts @@ -1,5 +1,6 @@ -/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-use-before-define */ import { + CancellationToken, CodeAction, CodeActionKind, CodeActionParams, @@ -8,64 +9,144 @@ import { Range, TextEdit, } from "vscode-languageserver"; -import { IElmWorkspace } from "../../elmWorkspace"; import { SyntaxNode } from "web-tree-sitter"; import { PositionUtil } from "../../positionUtil"; import { TreeUtils } from "../../util/treeUtils"; -import { Utils } from "../../util/utils"; import { URI } from "vscode-uri"; import { ElmWorkspaceMatcher } from "../../util/elmWorkspaceMatcher"; -import { TypeRenderer } from "../../util/types/typeRenderer"; +import { mapSyntaxNodeToExpression } from "../../util/types/expressionTree"; +import { MultistepOperation } from "../../util/multistepOperation"; +import { TypeChecker } from "../../util/types/typeChecker"; import { ITreeContainer } from "../../forest"; +import { IElmWorkspace } from "../../elmWorkspace"; +import { + ICancellationToken, + ThrottledCancellationToken, +} from "../../cancellation"; +import { container } from "tsyringe"; export class TypeInferenceDiagnostics { TYPE_INFERENCE = "Type Inference"; private elmWorkspaceMatcher: ElmWorkspaceMatcher; + private changeSeq = 0; + + private operation: MultistepOperation; constructor() { this.elmWorkspaceMatcher = new ElmWorkspaceMatcher((uri) => uri); + this.operation = new MultistepOperation(container.resolve("Connection")); + } + + public change(): void { + this.changeSeq++; } - public createDiagnostics = ( + public getDiagnosticsForFile( treeContainer: ITreeContainer, elmWorkspace: IElmWorkspace, - ): Diagnostic[] => { - let diagnostics: Diagnostic[] = []; - - const allTopLevelFunctions = TreeUtils.findAllTopLevelFunctionDeclarationsWithoutTypeAnnotation( - treeContainer.tree, - ); - + cancellationToken: CancellationToken, + ): Promise { const checker = elmWorkspace.getTypeChecker(); - if (allTopLevelFunctions) { - const inferencedTypes = allTopLevelFunctions - .filter(Utils.notUndefinedOrNull.bind(this)) - .map((func) => func.firstChild) - .filter(Utils.notUndefinedOrNull.bind(this)) - .map((node) => { - const typeString: string = checker.typeToString( - checker.findType(node, treeContainer.uri), - treeContainer, - ); - - if (typeString && typeString !== "Unknown" && node.firstNamedChild) { - return { - range: this.getNodeRange(node.firstNamedChild), - message: `Missing type annotation: \`${typeString}\``, - severity: DiagnosticSeverity.Information, - source: this.TYPE_INFERENCE, - }; + const diagnostics: Diagnostic[] = []; + + const allTopLevelFunctions = + TreeUtils.findAllTopLevelFunctionDeclarations(treeContainer.tree) ?? []; + + return new Promise((resolve) => { + this.operation.startNew( + cancellationToken, + (next) => { + const seq = this.changeSeq; + + let index = 0; + const goNext = (): void => { + index++; + if (allTopLevelFunctions.length > index) { + next.immediate(checkOne); + } + }; + + const checkOne = (): void => { + if (this.changeSeq !== seq) { + return; + } + + diagnostics.push( + ...this.getDiagnosticsForDeclaration( + checker, + allTopLevelFunctions[index], + treeContainer, + new ThrottledCancellationToken(cancellationToken), + ), + ); + + goNext(); + }; + + if (allTopLevelFunctions.length > 0 && this.changeSeq === seq) { + next.immediate(checkOne); } - }) - .filter(Utils.notUndefined.bind(this)); + }, + () => resolve(diagnostics), + ); + }); + } - diagnostics = inferencedTypes ?? []; + public getDiagnosticsForDeclaration( + checker: TypeChecker, + declaration: SyntaxNode, + treeContainer: ITreeContainer, + cancellationToken: ICancellationToken, + ): Diagnostic[] { + const valueDeclaration = mapSyntaxNodeToExpression(declaration); + if (valueDeclaration?.nodeType !== "ValueDeclaration") { + return []; + } + + const diagnostics: Diagnostic[] = []; + + checker + .getDiagnosticsFromDeclaration(valueDeclaration, cancellationToken) + .forEach((diagnostic) => { + const nodeUri = diagnostic.node.tree.uri; + + if (nodeUri === treeContainer.uri) { + diagnostics.push({ + range: { + start: this.getNodeRange(diagnostic.node).start, + end: this.getNodeRange(diagnostic.endNode).end, + }, + message: diagnostic.message, + severity: DiagnosticSeverity.Error, + source: this.TYPE_INFERENCE, + }); + } + }); + + if (!valueDeclaration.typeAnnotation) { + const typeString: string = checker.typeToString( + checker.findType(declaration), + treeContainer, + ); + + if ( + typeString && + typeString !== "unknown" && + declaration.firstNamedChild?.firstNamedChild + ) { + diagnostics.push({ + range: this.getNodeRange(declaration.firstNamedChild.firstNamedChild), + message: `Missing type annotation: \`${typeString}\``, + severity: DiagnosticSeverity.Information, + source: this.TYPE_INFERENCE, + }); + } } return diagnostics; - }; + } public onCodeAction(params: CodeActionParams): CodeAction[] { const { uri } = params.textDocument; @@ -107,7 +188,7 @@ export class TypeInferenceDiagnostics { if (nodeAtPosition.parent) { const typeString: string = checker.typeToString( - checker.findType(nodeAtPosition.parent, uri), + checker.findType(nodeAtPosition.parent), treeContainer, ); diff --git a/src/providers/documentFormatingProvider.ts b/src/providers/documentFormatingProvider.ts index 61c9967e..3a4af099 100644 --- a/src/providers/documentFormatingProvider.ts +++ b/src/providers/documentFormatingProvider.ts @@ -1,10 +1,12 @@ import { container, injectable } from "tsyringe"; import { + CancellationToken, DocumentFormattingParams, - IConnection, + Connection, TextEdit, } from "vscode-languageserver"; import { URI } from "vscode-uri"; +import { DiagnosticsProvider } from "."; import { IElmWorkspace } from "../elmWorkspace"; import * as Diff from "../util/diff"; import { execCmd } from "../util/elmUtils"; @@ -17,17 +19,21 @@ type DocumentFormattingResult = Promise; @injectable() export class DocumentFormattingProvider { private events: TextDocumentEvents; - private connection: IConnection; + private connection: Connection; private settings: Settings; + private diagnostics: DiagnosticsProvider; constructor() { this.settings = container.resolve("Settings"); - this.connection = container.resolve("Connection"); + this.connection = container.resolve("Connection"); this.events = container.resolve(TextDocumentEvents); - this.connection.onDocumentFormatting( - new ElmWorkspaceMatcher((params: DocumentFormattingParams) => - URI.parse(params.textDocument.uri), - ).handlerForWorkspace(this.handleFormattingRequest), + this.diagnostics = container.resolve(DiagnosticsProvider); + this.connection.onDocumentFormatting((params) => + this.diagnostics.interuptDiagnostics(() => + new ElmWorkspaceMatcher((params: DocumentFormattingParams) => + URI.parse(params.textDocument.uri), + ).handlerForWorkspace(this.handleFormattingRequest)(params), + ), ); } diff --git a/src/providers/documentSymbolProvider.ts b/src/providers/documentSymbolProvider.ts index a9e615ba..e0e61cc7 100644 --- a/src/providers/documentSymbolProvider.ts +++ b/src/providers/documentSymbolProvider.ts @@ -1,8 +1,9 @@ import { container } from "tsyringe"; import { + CancellationToken, DocumentSymbol, DocumentSymbolParams, - IConnection, + Connection, SymbolInformation, } from "vscode-languageserver"; import { URI } from "vscode-uri"; @@ -10,6 +11,7 @@ import { SyntaxNode, Tree } from "web-tree-sitter"; import { IElmWorkspace } from "../elmWorkspace"; import { ElmWorkspaceMatcher } from "../util/elmWorkspaceMatcher"; import { SymbolInformationTranslator } from "../util/symbolTranslator"; +import { ThrottledCancellationToken } from "../cancellation"; type DocumentSymbolResult = | SymbolInformation[] @@ -18,10 +20,10 @@ type DocumentSymbolResult = | undefined; export class DocumentSymbolProvider { - private connection: IConnection; + private connection: Connection; constructor() { - this.connection = container.resolve("Connection"); + this.connection = container.resolve("Connection"); this.connection.onDocumentSymbol( new ElmWorkspaceMatcher((param: DocumentSymbolParams) => URI.parse(param.textDocument.uri), @@ -32,6 +34,7 @@ export class DocumentSymbolProvider { private handleDocumentSymbolRequest = ( param: DocumentSymbolParams, elmWorkspace: IElmWorkspace, + token?: CancellationToken, ): DocumentSymbolResult => { this.connection.console.info(`Document Symbols were requested`); const symbolInformationList: SymbolInformation[] = []; @@ -39,7 +42,13 @@ export class DocumentSymbolProvider { const forest = elmWorkspace.getForest(); const tree: Tree | undefined = forest.getTree(param.textDocument.uri); + const cancellationToken = token + ? new ThrottledCancellationToken(token) + : undefined; + const traverse: (node: SyntaxNode) => void = (node: SyntaxNode): void => { + cancellationToken?.throwIfCancellationRequested(); + const symbolInformation = SymbolInformationTranslator.translateNodeToSymbolInformation( param.textDocument.uri, node, diff --git a/src/providers/foldingProvider.ts b/src/providers/foldingProvider.ts index 293ef897..aa915444 100644 --- a/src/providers/foldingProvider.ts +++ b/src/providers/foldingProvider.ts @@ -2,8 +2,8 @@ import { container } from "tsyringe"; import { FoldingRange, FoldingRangeKind, - FoldingRangeRequestParam, - IConnection, + Connection, + FoldingRangeParams, } from "vscode-languageserver"; import { URI } from "vscode-uri"; import { SyntaxNode, Tree } from "web-tree-sitter"; @@ -19,18 +19,18 @@ export class FoldingRangeProvider { "record_expr", "case_of_branch", ]); - private connection: IConnection; + private connection: Connection; constructor() { - this.connection = container.resolve("Connection"); + this.connection = container.resolve("Connection"); this.connection.onFoldingRanges( - new ElmWorkspaceMatcher((param: FoldingRangeRequestParam) => + new ElmWorkspaceMatcher((param: FoldingRangeParams) => URI.parse(param.textDocument.uri), ).handlerForWorkspace(this.handleFoldingRange), ); } protected handleFoldingRange = ( - param: FoldingRangeRequestParam, + param: FoldingRangeParams, elmWorkspace: IElmWorkspace, ): FoldingRange[] => { this.connection.console.info(`Folding ranges were requested`); diff --git a/src/providers/handlers/exposeUnexposeHandler.ts b/src/providers/handlers/exposeUnexposeHandler.ts index 17aed13d..2d0d4552 100644 --- a/src/providers/handlers/exposeUnexposeHandler.ts +++ b/src/providers/handlers/exposeUnexposeHandler.ts @@ -1,5 +1,5 @@ import { container } from "tsyringe"; -import { IConnection } from "vscode-languageserver"; +import { Connection } from "vscode-languageserver"; import { URI } from "vscode-uri"; import { IElmWorkspace } from "../../elmWorkspace"; import { @@ -11,7 +11,7 @@ import { ElmWorkspaceMatcher } from "../../util/elmWorkspaceMatcher"; import { RefactorEditUtils } from "../../util/refactorEditUtils"; export class ExposeUnexposeHandler { - private connection: IConnection; + private connection: Connection; constructor() { this.connection = container.resolve("Connection"); diff --git a/src/providers/handlers/fileEventsHandler.ts b/src/providers/handlers/fileEventsHandler.ts index a942a88a..5d933cbf 100644 --- a/src/providers/handlers/fileEventsHandler.ts +++ b/src/providers/handlers/fileEventsHandler.ts @@ -7,7 +7,7 @@ import { import { ElmWorkspaceMatcher } from "../../util/elmWorkspaceMatcher"; import { RefactorEditUtils } from "../../util/refactorEditUtils"; import { container } from "tsyringe"; -import { IConnection } from "vscode-languageserver"; +import { CancellationToken, Connection } from "vscode-languageserver"; import { URI } from "vscode-uri"; import { RenameUtils } from "../../util/renameUtils"; import { RenameProvider } from "../renameProvider"; @@ -15,10 +15,10 @@ import { TreeUtils } from "../../util/treeUtils"; import { PositionUtil } from "../../positionUtil"; export class FileEventsHandler { - private connection: IConnection; + private connection: Connection; constructor() { - this.connection = container.resolve("Connection"); + this.connection = container.resolve("Connection"); this.connection.onRequest(OnDidCreateFilesRequest, async (params) => { for (const file of params.files) { diff --git a/src/providers/handlers/moveRefactoringHandler.ts b/src/providers/handlers/moveRefactoringHandler.ts index bacc2ae6..f1af94b4 100644 --- a/src/providers/handlers/moveRefactoringHandler.ts +++ b/src/providers/handlers/moveRefactoringHandler.ts @@ -1,5 +1,5 @@ import { container } from "tsyringe"; -import { IConnection, Position, Range, TextEdit } from "vscode-languageserver"; +import { Connection, Position, Range, TextEdit } from "vscode-languageserver"; import { URI } from "vscode-uri"; import { IElmWorkspace } from "../../elmWorkspace"; import { @@ -15,7 +15,7 @@ import { References } from "../../util/references"; import { TreeUtils } from "../../util/treeUtils"; export class MoveRefactoringHandler { - private connection: IConnection; + private connection: Connection; constructor() { this.connection = container.resolve("Connection"); diff --git a/src/providers/hoverProvider.ts b/src/providers/hoverProvider.ts index b39e5d4a..d31160dd 100644 --- a/src/providers/hoverProvider.ts +++ b/src/providers/hoverProvider.ts @@ -1,12 +1,13 @@ import { container } from "tsyringe"; import { Hover, - IConnection, + Connection, MarkupKind, TextDocumentPositionParams, } from "vscode-languageserver"; import { URI } from "vscode-uri"; import { SyntaxNode } from "web-tree-sitter"; +import { DiagnosticsProvider } from "."; import { IElmWorkspace } from "../elmWorkspace"; import { getEmptyTypes } from "../util/elmUtils"; import { ElmWorkspaceMatcher } from "../util/elmWorkspaceMatcher"; @@ -16,14 +17,18 @@ import { NodeType, TreeUtils } from "../util/treeUtils"; type HoverResult = Hover | null | undefined; export class HoverProvider { - private connection: IConnection; + private connection: Connection; + private diagnostics: DiagnosticsProvider; constructor() { - this.connection = container.resolve("Connection"); - this.connection.onHover( - new ElmWorkspaceMatcher((param: TextDocumentPositionParams) => - URI.parse(param.textDocument.uri), - ).handlerForWorkspace(this.handleHoverRequest), + this.connection = container.resolve("Connection"); + this.diagnostics = container.resolve(DiagnosticsProvider); + this.connection.onHover((params) => + this.diagnostics.interuptDiagnostics(() => + new ElmWorkspaceMatcher((params: TextDocumentPositionParams) => + URI.parse(params.textDocument.uri), + ).handlerForWorkspace(this.handleHoverRequest)(params), + ), ); } diff --git a/src/providers/index.ts b/src/providers/index.ts index 702216ac..765c8cab 100644 --- a/src/providers/index.ts +++ b/src/providers/index.ts @@ -4,7 +4,6 @@ export * from "./codeLensProvider"; export * from "./completionProvider"; export * from "./definitionProvider"; export * from "./diagnostics/diagnosticsProvider"; -export * from "./diagnostics/elmAnalyseDiagnostics"; export * from "./diagnostics/elmMakeDiagnostics"; export * from "./diagnostics/typeInferenceDiagnostics"; export * from "./documentFormatingProvider"; diff --git a/src/providers/referencesProvider.ts b/src/providers/referencesProvider.ts index d65916ac..ba25917d 100644 --- a/src/providers/referencesProvider.ts +++ b/src/providers/referencesProvider.ts @@ -1,6 +1,6 @@ import { container } from "tsyringe"; import { - IConnection, + Connection, Location, Position, Range, @@ -16,9 +16,9 @@ import { TreeUtils } from "../util/treeUtils"; type ReferenceResult = Location[] | null | undefined; export class ReferencesProvider { - private connection: IConnection; + private connection: Connection; constructor() { - this.connection = container.resolve("Connection"); + this.connection = container.resolve("Connection"); this.connection.onReferences( new ElmWorkspaceMatcher((param: ReferenceParams) => URI.parse(param.textDocument.uri), diff --git a/src/providers/renameProvider.ts b/src/providers/renameProvider.ts index 39f46d12..71e304f6 100644 --- a/src/providers/renameProvider.ts +++ b/src/providers/renameProvider.ts @@ -1,6 +1,7 @@ import { container } from "tsyringe"; import { - IConnection, + Connection, + OptionalVersionedTextDocumentIdentifier, Position, PrepareRenameParams, Range, @@ -8,7 +9,6 @@ import { RenameParams, TextDocumentEdit, TextEdit, - VersionedTextDocumentIdentifier, WorkspaceEdit, } from "vscode-languageserver"; import { URI } from "vscode-uri"; @@ -18,10 +18,10 @@ import { ElmWorkspaceMatcher } from "../util/elmWorkspaceMatcher"; import { RenameUtils } from "../util/renameUtils"; export class RenameProvider { - private connection: IConnection; + private connection: Connection; constructor() { - this.connection = container.resolve("Connection"); + this.connection = container.resolve("Connection"); this.connection.onPrepareRename( new ElmWorkspaceMatcher((params: PrepareRenameParams) => URI.parse(params.textDocument.uri), @@ -144,7 +144,7 @@ export class RenameProvider { const element = edits[key]; textDocumentEdits.push( TextDocumentEdit.create( - VersionedTextDocumentIdentifier.create(key, null), + OptionalVersionedTextDocumentIdentifier.create(key, null), element, ), ); diff --git a/src/providers/selectionRangeProvider.ts b/src/providers/selectionRangeProvider.ts index 76efab23..9ebea8a9 100644 --- a/src/providers/selectionRangeProvider.ts +++ b/src/providers/selectionRangeProvider.ts @@ -1,6 +1,6 @@ import { container } from "tsyringe"; import { - IConnection, + Connection, Position, Range, SelectionRange, @@ -14,10 +14,10 @@ import { ElmWorkspaceMatcher } from "../util/elmWorkspaceMatcher"; import { TreeUtils } from "../util/treeUtils"; export class SelectionRangeProvider { - private connection: IConnection; + private connection: Connection; constructor() { - this.connection = container.resolve("Connection"); + this.connection = container.resolve("Connection"); this.connection.onSelectionRanges( new ElmWorkspaceMatcher((param: SelectionRangeParams) => URI.parse(param.textDocument.uri), diff --git a/src/providers/workspaceSymbolProvider.ts b/src/providers/workspaceSymbolProvider.ts index ba55cb2d..141b80c4 100644 --- a/src/providers/workspaceSymbolProvider.ts +++ b/src/providers/workspaceSymbolProvider.ts @@ -1,6 +1,6 @@ import { container } from "tsyringe"; import { - IConnection, + Connection, SymbolInformation, WorkspaceSymbolParams, } from "vscode-languageserver"; @@ -9,12 +9,12 @@ import { IElmWorkspace } from "../elmWorkspace"; import { SymbolInformationTranslator } from "../util/symbolTranslator"; export class WorkspaceSymbolProvider { - private readonly connection: IConnection; + private readonly connection: Connection; private readonly elmWorkspaces: IElmWorkspace[]; constructor() { this.elmWorkspaces = container.resolve("ElmWorkspaces"); - this.connection = container.resolve("Connection"); + this.connection = container.resolve("Connection"); this.connection.onWorkspaceSymbol(this.workspaceSymbolRequest); } diff --git a/src/server.ts b/src/server.ts index fe296285..4f820514 100644 --- a/src/server.ts +++ b/src/server.ts @@ -2,11 +2,11 @@ import globby from "globby"; import path from "path"; import { container } from "tsyringe"; import { - IConnection, + Connection, InitializeParams, InitializeResult, + WorkDoneProgressReporter, } from "vscode-languageserver"; -import { WorkDoneProgress } from "vscode-languageserver/lib/progress"; import { URI } from "vscode-uri"; import { CapabilityCalculator } from "./capabilityCalculator"; import { ElmWorkspace, IElmWorkspace } from "./elmWorkspace"; @@ -19,7 +19,6 @@ import { DiagnosticsProvider, DocumentFormattingProvider, DocumentSymbolProvider, - ElmAnalyseDiagnostics, ElmMakeDiagnostics, FoldingRangeProvider, HoverProvider, @@ -39,9 +38,12 @@ export interface ILanguageServer { } export class Server implements ILanguageServer { - private connection: IConnection; + private connection: Connection; - constructor(params: InitializeParams, private progress: WorkDoneProgress) { + constructor( + params: InitializeParams, + private progress: WorkDoneProgressReporter, + ) { this.connection = container.resolve("Connection"); const uri = this.getWorkspaceUri(params); @@ -123,6 +125,20 @@ export class Server implements ILanguageServer { // We can now query the client for up to date settings settings.initFinished(); + const clientSettings = await settings.getClientSettings(); + + container.register("ClientSettings", { + useValue: clientSettings, + }); + + container.register(ASTProvider, { + useValue: new ASTProvider(), + }); + + container.register(DiagnosticsProvider, { + useValue: new DiagnosticsProvider(), + }); + // these register calls rely on settings having been setup container.register(DocumentFormattingProvider, { useValue: new DocumentFormattingProvider(), @@ -140,25 +156,6 @@ export class Server implements ILanguageServer { useValue: new ElmLsDiagnostics(), }); - const clientSettings = await settings.getClientSettings(); - - container.register("ClientSettings", { - useValue: clientSettings, - }); - - container.register(ElmAnalyseDiagnostics, { - useValue: - clientSettings.elmAnalyseTrigger !== "never" - ? new ElmAnalyseDiagnostics() - : null, - }); - - container.register(ASTProvider, { - useValue: new ASTProvider(), - }); - - new DiagnosticsProvider(); - new CodeActionProvider(); new FoldingRangeProvider(); diff --git a/src/util/delayer.ts b/src/util/delayer.ts new file mode 100644 index 00000000..a4b6b639 --- /dev/null +++ b/src/util/delayer.ts @@ -0,0 +1,66 @@ +/** + * Delayer taken from VS Code typescript-language-features: https://github.com/microsoft/vscode/blob/a36c68b9ec3d6a0aca9799d7a10be741a6658a51/extensions/typescript-language-features/src/utils/async.ts#L10 + */ + +export interface ITask { + (): T; +} + +export class Delayer { + public defaultDelay: number; + private timeout: NodeJS.Timeout | null; // Timer + private completionPromise: Promise | null; + private onSuccess: ((value: T | PromiseLike | undefined) => void) | null; + private task: ITask | null; + + constructor(defaultDelay: number) { + this.defaultDelay = defaultDelay; + this.timeout = null; + this.completionPromise = null; + this.onSuccess = null; + this.task = null; + } + + public trigger( + task: ITask, + delay: number = this.defaultDelay, + ): Promise { + this.task = task; + if (delay >= 0) { + this.cancelTimeout(); + } + + if (!this.completionPromise) { + this.completionPromise = new Promise((resolve) => { + this.onSuccess = resolve; + }).then(() => { + this.completionPromise = null; + this.onSuccess = null; + const result = this.task && this.task(); + this.task = null; + return result; + }); + } + + if (delay >= 0 || this.timeout === null) { + this.timeout = setTimeout( + () => { + this.timeout = null; + if (this.onSuccess) { + this.onSuccess(undefined); + } + }, + delay >= 0 ? delay : this.defaultDelay, + ); + } + + return this.completionPromise; + } + + private cancelTimeout(): void { + if (this.timeout !== null) { + clearTimeout(this.timeout); + this.timeout = null; + } + } +} diff --git a/src/util/documentEvents.ts b/src/util/documentEvents.ts index 9a5a0e41..72e2c046 100644 --- a/src/util/documentEvents.ts +++ b/src/util/documentEvents.ts @@ -4,7 +4,7 @@ import { DidCloseTextDocumentParams, DidOpenTextDocumentParams, DidSaveTextDocumentParams, - IConnection, + Connection, } from "vscode-languageserver"; import { injectable, container } from "tsyringe"; @@ -23,7 +23,7 @@ export interface IDocumentEvents { @injectable() export class DocumentEvents extends EventEmitter implements IDocumentEvents { constructor() { - const connection = container.resolve("Connection"); + const connection = container.resolve("Connection"); super(); connection.onDidChangeTextDocument((e) => this.emit("change", e)); diff --git a/src/util/elmUtils.ts b/src/util/elmUtils.ts index 001bffa4..8b6a1b99 100644 --- a/src/util/elmUtils.ts +++ b/src/util/elmUtils.ts @@ -1,6 +1,6 @@ import execa, { ExecaReturnValue } from "execa"; import * as path from "path"; -import { IConnection, CompletionItemKind } from "vscode-languageserver"; +import { Connection, CompletionItemKind } from "vscode-languageserver"; import { URI } from "vscode-uri"; import { IClientSettings } from "./settings"; @@ -23,7 +23,7 @@ export async function execCmd( cmdStatic: string, options: IExecCmdOptions = {}, cwd: string, - connection: IConnection, + connection: Connection, input?: string, ): Promise> { const cmd = cmdFromUser === "" ? cmdStatic : cmdFromUser; @@ -86,7 +86,7 @@ export function getEmptyTypes(): { export async function getElmVersion( settings: IClientSettings, elmWorkspaceFolder: URI, - connection: IConnection, + connection: Connection, ): Promise { const options = { cmdArguments: ["--version"], diff --git a/src/util/elmWorkspaceMatcher.ts b/src/util/elmWorkspaceMatcher.ts index a735b435..a4ff9a61 100644 --- a/src/util/elmWorkspaceMatcher.ts +++ b/src/util/elmWorkspaceMatcher.ts @@ -1,4 +1,5 @@ import { container } from "tsyringe"; +import { CancellationToken } from "vscode-languageserver"; import { URI } from "vscode-uri"; import { IElmWorkspace } from "../elmWorkspace"; import { NoWorkspaceContainsError } from "./noWorkspaceContainsError"; @@ -16,10 +17,14 @@ export class ElmWorkspaceMatcher { } public handlerForWorkspace( - handler: (param: ParamType, elmWorkspace: IElmWorkspace) => ResultType, - ): (param: ParamType) => ResultType { - return (param: ParamType): ResultType => { - return handler(param, this.getElmWorkspaceFor(param)); + handler: ( + param: ParamType, + elmWorkspace: IElmWorkspace, + token?: CancellationToken, + ) => ResultType, + ): (param: ParamType, token?: CancellationToken) => ResultType { + return (param: ParamType, token?: CancellationToken): ResultType => { + return handler(param, this.getElmWorkspaceFor(param), token); }; } diff --git a/src/util/multistepOperation.ts b/src/util/multistepOperation.ts new file mode 100644 index 00000000..43bd1623 --- /dev/null +++ b/src/util/multistepOperation.ts @@ -0,0 +1,97 @@ +import { CancellationToken, Connection } from "vscode-languageserver"; +import { OperationCanceledException } from "../cancellation"; + +/** + * MultistepOperation taken from Typescript: https://github.com/microsoft/TypeScript/blob/79ffd03f8b73010fa03cef624e5f1770bc9c975b/src/server/session.ts#L166 + */ + +interface INextStep { + immediate(action: () => void): void; + delay(ms: number, action: () => void): void; +} + +export class MultistepOperation implements INextStep { + private timerHandle: NodeJS.Timeout | undefined; + private immediateId: NodeJS.Immediate | undefined; + private cancellationToken: CancellationToken | undefined; + private done: (() => void) | undefined; + + constructor(private connection: Connection) {} + + public startNew( + cancellationToken: CancellationToken, + action: (next: INextStep) => void, + done: () => void, + ): void { + this.complete(); + this.cancellationToken = cancellationToken; + this.done = done; + this.executeAction(action); + } + + private complete(): void { + if (this.done !== undefined) { + this.done(); + this.cancellationToken = undefined; + this.done = undefined; + } + this.setTimerHandle(undefined); + this.setImmediateId(undefined); + } + + public immediate(action: () => void): void { + this.setImmediateId( + setImmediate(() => { + this.immediateId = undefined; + this.executeAction(action); + }), + ); + } + + public delay(ms: number, action: () => void): void { + this.setTimerHandle( + setTimeout(() => { + this.timerHandle = undefined; + this.executeAction(action); + }, ms), + ); + } + + private executeAction(action: (next: INextStep) => void): void { + let stop = false; + try { + if (this.cancellationToken?.isCancellationRequested) { + stop = true; + } else { + action(this); + } + } catch (e) { + stop = true; + // ignore cancellation request + if (!(e instanceof OperationCanceledException)) { + this.connection.console.error(`${e} delayed processing of request`); + } + } + if (stop || !this.hasPendingWork()) { + this.complete(); + } + } + + private setTimerHandle(timerHandle: NodeJS.Timeout | undefined): void { + if (this.timerHandle !== undefined) { + clearTimeout(this.timerHandle); + } + this.timerHandle = timerHandle; + } + + private setImmediateId(immediateId: NodeJS.Immediate | undefined): void { + if (this.immediateId !== undefined) { + clearImmediate(this.immediateId); + } + this.immediateId = immediateId; + } + + private hasPendingWork(): boolean { + return !!this.timerHandle || !!this.immediateId; + } +} diff --git a/src/util/settings.ts b/src/util/settings.ts index b188cbc6..53711e0f 100644 --- a/src/util/settings.ts +++ b/src/util/settings.ts @@ -1,4 +1,4 @@ -import { ClientCapabilities, IConnection } from "vscode-languageserver"; +import { ClientCapabilities, Connection } from "vscode-languageserver"; import { injectable, container } from "tsyringe"; export interface IClientSettings { @@ -14,6 +14,7 @@ export interface IClientSettings { export interface IExtendedCapabilites { moveFunctionRefactoringSupport: boolean; exposeUnexposeSupport: boolean; + clientInitiatedDiagnostics: boolean; } export type ElmAnalyseTrigger = "change" | "save" | "never"; @@ -28,7 +29,7 @@ export class Settings { trace: { server: "off" }, disableElmLSDiagnostics: false, }; - private connection: IConnection; + private connection: Connection; private initDone = false; @@ -36,7 +37,7 @@ export class Settings { config: IClientSettings, private clientCapabilities: ClientCapabilities, ) { - this.connection = container.resolve("Connection"); + this.connection = container.resolve("Connection"); this.updateSettings(config); } diff --git a/src/util/textDocumentEvents.ts b/src/util/textDocumentEvents.ts index b0dabc18..97965f97 100644 --- a/src/util/textDocumentEvents.ts +++ b/src/util/textDocumentEvents.ts @@ -85,4 +85,8 @@ export class TextDocumentEvents extends EventEmitter { public get(uri: string): TextDocument | undefined { return this._documents[uri]; } + + public getManagedUris(): string[] { + return Object.keys(this._documents); + } } diff --git a/src/util/treeUtils.ts b/src/util/treeUtils.ts index 5eabdd2f..b5699153 100644 --- a/src/util/treeUtils.ts +++ b/src/util/treeUtils.ts @@ -3,7 +3,7 @@ import { SyntaxNode, Tree } from "web-tree-sitter"; import { ITreeContainer } from "../forest"; import { comparePosition } from "../positionUtil"; import { Type } from "./types/typeInference"; -import { IElmWorkspace } from "src/elmWorkspace"; +import { IElmWorkspace } from "../elmWorkspace"; export type NodeType = | "Function" diff --git a/src/util/types/binder.ts b/src/util/types/binder.ts index c462547f..f60cec41 100644 --- a/src/util/types/binder.ts +++ b/src/util/types/binder.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/no-use-before-define */ -import { ITreeContainer } from "src/forest"; +import { ITreeContainer } from "../../forest"; import { SyntaxNode } from "web-tree-sitter"; import { MultiMap } from "../multiMap"; import { IExposed, IExposing, NodeType, TreeUtils } from "../treeUtils"; diff --git a/src/util/types/expressionTree.ts b/src/util/types/expressionTree.ts index fe8f42dc..c9e4f6c5 100644 --- a/src/util/types/expressionTree.ts +++ b/src/util/types/expressionTree.ts @@ -2,7 +2,7 @@ import { SyntaxNode } from "web-tree-sitter"; import { OperatorAssociativity } from "./operatorPrecedence"; import { TreeUtils } from "../treeUtils"; import { Utils } from "../utils"; -import { IElmWorkspace } from "src/elmWorkspace"; +import { IElmWorkspace } from "../../elmWorkspace"; import { performance } from "perf_hooks"; /* eslint-disable @typescript-eslint/naming-convention */ diff --git a/src/util/types/typeChecker.ts b/src/util/types/typeChecker.ts index 3f609948..0eda53a6 100644 --- a/src/util/types/typeChecker.ts +++ b/src/util/types/typeChecker.ts @@ -9,7 +9,7 @@ import { } from "./expressionTree"; import { IElmWorkspace } from "../../elmWorkspace"; import { container } from "tsyringe"; -import { IConnection } from "vscode-languageserver"; +import { Connection } from "vscode-languageserver"; import { Type, TUnknown, @@ -25,6 +25,7 @@ import { bindTreeContainer } from "./binder"; import { Sequence } from "../sequence"; import { Utils } from "../utils"; import { TypeExpression } from "./typeExpression"; +import { ICancellationToken } from "../../cancellation"; export let bindTime = 0; export function resetBindTime(): void { @@ -38,8 +39,7 @@ export interface DefinitionResult { } export interface TypeChecker { - findType: (node: SyntaxNode, uri: string) => Type; - // getExposedForModule: (moduleName: string) => IExposing[]; + findType: (node: SyntaxNode) => Type; findDefinition: ( node: SyntaxNode, treeContainer: ITreeContainer, @@ -55,7 +55,14 @@ export interface TypeChecker { name: string, ) => string | undefined; typeToString: (t: Type, treeContainer?: ITreeContainer) => string; - getDiagnostics: (treeContainer: ITreeContainer) => Diagnostic[]; + getDiagnostics: ( + treeContainer: ITreeContainer, + cancellationToken?: ICancellationToken, + ) => Diagnostic[]; + getDiagnosticsFromDeclaration: ( + valueDeclaration: SyntaxNode, + cancellationToken?: ICancellationToken, + ) => Diagnostic[]; findImportModuleNameNode: ( moduleNameOrAlias: string, treeContainer: ITreeContainer, @@ -80,17 +87,20 @@ export function createTypeChecker(workspace: IElmWorkspace): TypeChecker { getQualifierForName, typeToString, getDiagnostics, + getDiagnosticsFromDeclaration, findImportModuleNameNode, }; return typeChecker; - function findType(node: SyntaxNode, uri: string): Type { + function findType(node: SyntaxNode): Type { try { const declaration = mapSyntaxNodeToExpression( TreeUtils.findParentOfType("value_declaration", node, true), ); + const uri = node.tree.uri; + const findTypeOrParentType = ( expr: SyntaxNode | undefined, inferenceResult: InferenceResult, @@ -183,13 +193,16 @@ export function createTypeChecker(workspace: IElmWorkspace): TypeChecker { return TUnknown; } catch (error) { - const connection = container.resolve("Connection"); + const connection = container.resolve("Connection"); connection.console.warn(`Error while trying to infer a type. ${error}`); return TUnknown; } } - function getDiagnostics(treeContainer: ITreeContainer): Diagnostic[] { + function getDiagnostics( + treeContainer: ITreeContainer, + cancellationToken?: ICancellationToken, + ): Diagnostic[] { const allTopLevelFunctions = TreeUtils.findAllTopLevelFunctionDeclarations( treeContainer.tree, ); @@ -197,16 +210,10 @@ export function createTypeChecker(workspace: IElmWorkspace): TypeChecker { return ( allTopLevelFunctions ?.map((valueDeclaration) => { - try { - return InferenceScope.valueDeclarationInference( - mapSyntaxNodeToExpression(valueDeclaration) as EValueDeclaration, - treeContainer.uri, - workspace, - new Set(), - ).diagnostics; - } catch { - return []; - } + return getDiagnosticsFromDeclaration( + valueDeclaration, + cancellationToken, + ); }) .reduce((a, b) => a.concat(b), []) .filter( @@ -215,6 +222,28 @@ export function createTypeChecker(workspace: IElmWorkspace): TypeChecker { ); } + function getDiagnosticsFromDeclaration( + valueDeclaration: SyntaxNode, + cancellationToken?: ICancellationToken, + ): Diagnostic[] { + if (valueDeclaration.type !== "value_declaration") { + throw new Error( + "getDiagnosticsFromDeclaration must take a node of type value_declaraion", + ); + } + try { + return InferenceScope.valueDeclarationInference( + mapSyntaxNodeToExpression(valueDeclaration) as EValueDeclaration, + valueDeclaration.tree.uri, + workspace, + new Set(), + cancellationToken, + ).diagnostics; + } catch { + return []; + } + } + function getAllImports(treeContainer: ITreeContainer): Imports { const cached = imports.get(treeContainer.uri); @@ -447,7 +476,7 @@ export function createTypeChecker(workspace: IElmWorkspace): TypeChecker { nodeParentType === "lower_pattern" && nodeParent.parent?.type === "record_pattern" ) { - const type = findType(nodeParent.parent, uri); + const type = findType(nodeParent.parent); return TreeUtils.findFieldReference(type, nodeText); } else if ( nodeParentType === "value_qid" || @@ -559,7 +588,7 @@ export function createTypeChecker(workspace: IElmWorkspace): TypeChecker { } if (target) { - const type = findType(target, uri); + const type = findType(target); return TreeUtils.findFieldReference(type, nodeText); } } else if ( @@ -567,13 +596,13 @@ export function createTypeChecker(workspace: IElmWorkspace): TypeChecker { nodeParentType === "field" && nodeParent.parent?.type === "record_expr" ) { - const type = findType(nodeParent.parent, uri); + const type = findType(nodeParent.parent); return TreeUtils.findFieldReference(type, nodeText); } else if ( nodeAtPosition.type === "lower_case_identifier" && nodeParentType === "field_accessor_function_expr" ) { - const type = findType(nodeParent, uri); + const type = findType(nodeParent); if (type.nodeType === "Function") { const paramType = type.params[0]; diff --git a/src/util/types/typeExpression.ts b/src/util/types/typeExpression.ts index 64556139..f3e9e436 100644 --- a/src/util/types/typeExpression.ts +++ b/src/util/types/typeExpression.ts @@ -36,7 +36,7 @@ import { TypeReplacement } from "./typeReplacement"; import { SyntaxNodeMap } from "./syntaxNodeMap"; import { Utils } from "../utils"; import { RecordFieldReferenceTable } from "./recordFieldReferenceTable"; -import { IElmWorkspace } from "src/elmWorkspace"; +import { IElmWorkspace } from "../../elmWorkspace"; export class TypeExpression { // All the type variables we've seen diff --git a/src/util/types/typeInference.ts b/src/util/types/typeInference.ts index bdc28d7d..d7917799 100644 --- a/src/util/types/typeInference.ts +++ b/src/util/types/typeInference.ts @@ -39,12 +39,13 @@ import { } from "./expressionTree"; import { SyntaxNodeMap } from "./syntaxNodeMap"; import { TypeExpression } from "./typeExpression"; -import { IElmWorkspace } from "src/elmWorkspace"; +import { IElmWorkspace } from "../../elmWorkspace"; import { Sequence } from "../sequence"; import { Utils } from "../utils"; import { RecordFieldReferenceTable } from "./recordFieldReferenceTable"; import { TypeChecker } from "./typeChecker"; import { performance } from "perf_hooks"; +import { ICancellationToken } from "../../cancellation"; export let inferTime = 0; export function resetInferTime(): void { @@ -624,6 +625,7 @@ export class InferenceScope { private nonShadowableNames: Set, private activeScopes: Set, private recursionAllowed: boolean, + private cancellationToken?: ICancellationToken, private parent?: InferenceScope, ) { this.replacements = parent?.replacements ?? new DisjointSet(); @@ -649,6 +651,7 @@ export class InferenceScope { uri: string, elmWorkspace: IElmWorkspace, activeScopes: Set, + cancellationToken?: ICancellationToken, ): InferenceResult { // TODO: Need a good way to get all visible values const shadowableNames = new Set(); @@ -660,6 +663,7 @@ export class InferenceScope { shadowableNames, new Set(activeScopes.values()), /* recursionAllowed */ false, + cancellationToken, ).inferDeclaration(declaration, true); const start = performance.now(); @@ -789,6 +793,8 @@ export class InferenceScope { private infer(e: Expression): Type { let type: Type = TUnknown; + this.cancellationToken?.throwIfCancellationRequested(); + switch (e.nodeType) { case "AnonymousFunctionExpr": type = this.inferLambda(e); @@ -918,6 +924,7 @@ export class InferenceScope { new Set(this.nonShadowableNames.values()), activeScopes, recursionAllowed, + this.cancellationToken, this, ), ); @@ -1168,6 +1175,7 @@ export class InferenceScope { referenceUri, this.elmWorkspace, this.activeScopes, + this.cancellationToken, ).type : parentScope.inferChildDeclaration(declaration, this.activeScopes) .type; diff --git a/test/completionProvider.test.ts b/test/completionProvider.test.ts index c1c237ef..374f3e6f 100644 --- a/test/completionProvider.test.ts +++ b/test/completionProvider.test.ts @@ -26,7 +26,6 @@ type exactCompletions = "exactMatch" | "partialMatch"; type dotCompletions = "triggeredByDot" | "normal"; describe("CompletionProvider", () => { - const completionProvider = new MockCompletionProvider(); const treeParser = new SourceTreeParser(); const debug = process.argv.find((arg) => arg === "--debug"); @@ -46,6 +45,7 @@ describe("CompletionProvider", () => { testDotCompletion: dotCompletions = "normal", ) { await treeParser.init(); + const completionProvider = new MockCompletionProvider(); const { newSources, position, fileWithCaret } = getCaretPositionFromSource( source, diff --git a/test/diagnosticTests/elmLsDiagnostics.test.ts b/test/diagnosticTests/elmLsDiagnostics.test.ts index a7004066..f639818e 100644 --- a/test/diagnosticTests/elmLsDiagnostics.test.ts +++ b/test/diagnosticTests/elmLsDiagnostics.test.ts @@ -35,7 +35,7 @@ describe("ElmLsDiagnostics", () => { } const diagnostics = elmDiagnostics - .createDiagnostics(treeContainer.tree, uri, workspace) + .createDiagnostics(treeContainer, workspace) .filter((diagnostic) => diagnostic.code === code); const diagnosticsEqual = Utils.arrayEquals( diff --git a/test/jest.setup.ts b/test/jest.setup.ts index edb95190..f53e101c 100644 --- a/test/jest.setup.ts +++ b/test/jest.setup.ts @@ -1,7 +1,16 @@ import "reflect-metadata"; import { container } from "tsyringe"; -import { IConnection } from "vscode-languageserver"; +import { Connection } from "vscode-languageserver"; import { mockDeep } from "jest-mock-extended"; +import { Settings } from "../src/util/settings"; +import { DocumentEvents } from "../src/util/documentEvents"; -container.register("Connection", { useValue: mockDeep() }); +container.register("Connection", { useValue: mockDeep() }); container.register("ElmWorkspaces", { useValue: [] }); +container.register("Settings", { + useValue: new Settings({} as any, {}), +}); +container.register("ClientSettings", { + useValue: {}, +}); +container.registerSingleton("DocumentEvents", DocumentEvents); diff --git a/test/performance.ts b/test/performance.ts index ed588a65..9a494a5f 100644 --- a/test/performance.ts +++ b/test/performance.ts @@ -23,6 +23,13 @@ import { replaceTime, resetReplaceTime, } from "../src/util/types/typeReplacement"; +import { + getCancellationFilePath, + FileBasedCancellationTokenSource, + getCancellationFolderPath, + ThrottledCancellationToken, +} from "../src/cancellation"; +import { randomBytes } from "crypto"; container.register("Connection", { useValue: { @@ -77,9 +84,20 @@ export async function runPerformanceTests(uri: string): Promise { // }); + const cancellationToken = new FileBasedCancellationTokenSource( + getCancellationFilePath( + getCancellationFolderPath(randomBytes(21).toString("hex")), + "1", + ), + ); + + const token = new ThrottledCancellationToken(cancellationToken.token); + elmWorkspace .getForest() - .treeMap.forEach(elmWorkspace.getTypeChecker().getDiagnostics); + .treeMap.forEach((treeContainer) => + elmWorkspace.getTypeChecker().getDiagnostics(treeContainer, token), + ); addTime("BINDING :", bindTime); addTime("INFER :", inferTime); diff --git a/test/typeInference.test.ts b/test/typeInference.test.ts index 75468fd8..82a74dc5 100644 --- a/test/typeInference.test.ts +++ b/test/typeInference.test.ts @@ -64,7 +64,7 @@ describe("test type inference", () => { } const checker = workspace.getTypeChecker(); - const nodeType = checker.findType(declaration, testUri); + const nodeType = checker.findType(declaration); expect(checker.typeToString(nodeType, treeContainer)).toEqual(expectedType); } diff --git a/tsconfig.json b/tsconfig.json index fdf79367..03eeef0f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,9 @@ { "compilerOptions": { - "baseUrl": ".", + // If we want to use baseUrl, we need to use a path rewrite module because + // Typescript does not rewrite import paths. This thread has some suggestions: + // https://github.com/microsoft/TypeScript/issues/10866 + // "baseUrl": ".", "target": "es6", "module": "commonjs", "moduleResolution": "node", @@ -14,7 +17,7 @@ "composite": true, "experimentalDecorators": true, "emitDecoratorMetadata": true, - "typeRoots": [ "./src/typings", "./node_modules/@types"] + "typeRoots": [ "./src/typings", "./node_modules/@types"], }, "include": ["src"], "exclude": ["node_modules", ".vscode-test", "**/*.test.ts"]