Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 15 additions & 15 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"version": "1.13.2",
"author": "Kolja Lampe",
"license": "MIT",
"main": "./out/module.js",
"files": [
"out"
],
Expand All @@ -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"
Expand Down
221 changes: 221 additions & 0 deletions src/cancellation.ts
Original file line number Diff line number Diff line change
@@ -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<any> | 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<any> {
if (!this._emitter) {
this._emitter = new Emitter<any>();
}
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();
}
}
}
7 changes: 1 addition & 6 deletions src/capabilityCalculator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions src/elmWorkspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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;
Expand Down
14 changes: 9 additions & 5 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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<IConnection>("Connection", {
useValue: createConnection(ProposedFeatures.all),
container.register<Connection>("Connection", {
useValue: createConnection(ProposedFeatures.all, {
cancellationStrategy: getCancellationStrategyFromArgv(process.argv),
}),
});
container.registerSingleton<Parser>("Parser", Parser);

Expand All @@ -40,7 +43,7 @@ container.register(TextDocumentEvents, {
useValue: new TextDocumentEvents(),
});

const connection = container.resolve<IConnection>("Connection");
const connection = container.resolve<Connection>("Connection");

let server: ILanguageServer;

Expand All @@ -62,6 +65,7 @@ connection.onInitialize(
container.register(CapabilityCalculator, {
useValue: new CapabilityCalculator(params.capabilities),
});

const initializationOptions = params.initializationOptions ?? {};

container.register("Settings", {
Expand Down
1 change: 1 addition & 0 deletions src/module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * as Protocol from "./protocol";
Loading