Skip to content

Commit 4742a70

Browse files
committed
Implement new diagnostics system
* Add interruptable diagnostic provider * Enable type inference compiler diagnostics * Remove elm-analyse
1 parent b34e7b3 commit 4742a70

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+1041
-821
lines changed

package-lock.json

+15-15
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"version": "1.13.2",
55
"author": "Kolja Lampe",
66
"license": "MIT",
7+
"main": "./out/module.js",
78
"files": [
89
"out"
910
],
@@ -24,7 +25,7 @@
2425
"reflect-metadata": "^0.1.13",
2526
"ts-debounce": "^2.0.1",
2627
"tsyringe": "^4.3.0",
27-
"vscode-languageserver": "^6.1.1",
28+
"vscode-languageserver": "^7.0.0-next.11",
2829
"vscode-languageserver-textdocument": "1.0.1",
2930
"vscode-uri": "^2.1.2",
3031
"web-tree-sitter": "^0.17.1"

src/cancellation.ts

+221
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
/* eslint-disable @typescript-eslint/no-use-before-define */
2+
import * as fs from "fs";
3+
import * as os from "os";
4+
import path from "path";
5+
import { performance } from "perf_hooks";
6+
import {
7+
AbstractCancellationTokenSource,
8+
CancellationId,
9+
CancellationReceiverStrategy,
10+
CancellationSenderStrategy,
11+
CancellationStrategy,
12+
CancellationToken,
13+
Emitter,
14+
Event,
15+
} from "vscode-languageserver";
16+
17+
/**
18+
* File based cancellation mostly taken from pyright: https://github.com./microsoft/pyright/blob/a9d2528574087cc2f8c10a7c3aaeb287eb64a870/packages/pyright-internal/src/common/cancellationUtils.ts#L48
19+
*/
20+
21+
class FileBasedToken implements CancellationToken {
22+
private _isCancelled = false;
23+
private _emitter: Emitter<any> | undefined;
24+
25+
constructor(private _cancellationFilePath: string) {}
26+
27+
public cancel(): void {
28+
if (!this._isCancelled) {
29+
this._isCancelled = true;
30+
if (this._emitter) {
31+
this._emitter.fire(undefined);
32+
this.dispose();
33+
}
34+
}
35+
}
36+
37+
get isCancellationRequested(): boolean {
38+
if (this._isCancelled) {
39+
return true;
40+
}
41+
42+
if (this._pipeExists()) {
43+
// the first time it encounters cancellation file, it will
44+
// cancel itself and raise cancellation event.
45+
// in this mode, cancel() might not be called explicitly by jsonrpc layer
46+
this.cancel();
47+
}
48+
49+
return this._isCancelled;
50+
}
51+
52+
get onCancellationRequested(): Event<any> {
53+
if (!this._emitter) {
54+
this._emitter = new Emitter<any>();
55+
}
56+
return this._emitter.event;
57+
}
58+
59+
public dispose(): void {
60+
if (this._emitter) {
61+
this._emitter.dispose();
62+
this._emitter = undefined;
63+
}
64+
}
65+
66+
private _pipeExists(): boolean {
67+
try {
68+
fs.statSync(this._cancellationFilePath);
69+
return true;
70+
} catch (e) {
71+
return false;
72+
}
73+
}
74+
}
75+
76+
export class FileBasedCancellationTokenSource
77+
implements AbstractCancellationTokenSource {
78+
private _token: CancellationToken | undefined;
79+
constructor(private _cancellationFilePath: string) {}
80+
81+
get token(): CancellationToken {
82+
if (!this._token) {
83+
// be lazy and create the token only when
84+
// actually needed
85+
this._token = new FileBasedToken(this._cancellationFilePath);
86+
}
87+
return this._token;
88+
}
89+
90+
cancel(): void {
91+
if (!this._token) {
92+
// save an object by returning the default
93+
// cancelled token when cancellation happens
94+
// before someone asks for the token
95+
this._token = CancellationToken.Cancelled;
96+
} else {
97+
(this._token as FileBasedToken).cancel();
98+
}
99+
}
100+
101+
dispose(): void {
102+
if (!this._token) {
103+
// ensure to initialize with an empty token if we had none
104+
this._token = CancellationToken.None;
105+
} else if (this._token instanceof FileBasedToken) {
106+
// actually dispose
107+
this._token.dispose();
108+
}
109+
}
110+
}
111+
112+
export function getCancellationFolderPath(folderName: string): string {
113+
return path.join(os.tmpdir(), "elm-language-server-cancellation", folderName);
114+
}
115+
116+
export function getCancellationFilePath(
117+
folderName: string,
118+
id: CancellationId,
119+
): string {
120+
return path.join(
121+
getCancellationFolderPath(folderName),
122+
`cancellation-${String(id)}.tmp`,
123+
);
124+
}
125+
126+
class FileCancellationReceiverStrategy implements CancellationReceiverStrategy {
127+
constructor(readonly folderName: string) {}
128+
129+
createCancellationTokenSource(
130+
id: CancellationId,
131+
): AbstractCancellationTokenSource {
132+
return new FileBasedCancellationTokenSource(
133+
getCancellationFilePath(this.folderName, id),
134+
);
135+
}
136+
}
137+
138+
let cancellationFolderName: string;
139+
140+
export function getCancellationStrategyFromArgv(
141+
argv: string[],
142+
): CancellationStrategy {
143+
let receiver: CancellationReceiverStrategy | undefined;
144+
145+
for (let i = 0; i < argv.length; i++) {
146+
const arg = argv[i];
147+
if (arg === "--cancellationReceive") {
148+
receiver = createReceiverStrategyFromArgv(argv[i + 1]);
149+
} else {
150+
const args = arg.split("=");
151+
if (args[0] === "--cancellationReceive") {
152+
receiver = createReceiverStrategyFromArgv(args[1]);
153+
}
154+
}
155+
}
156+
157+
if (receiver && !cancellationFolderName) {
158+
cancellationFolderName = (receiver as FileCancellationReceiverStrategy)
159+
.folderName;
160+
}
161+
162+
receiver = receiver ? receiver : CancellationReceiverStrategy.Message;
163+
return { receiver, sender: CancellationSenderStrategy.Message };
164+
165+
function createReceiverStrategyFromArgv(
166+
arg: string,
167+
): CancellationReceiverStrategy | undefined {
168+
const folderName = extractCancellationFolderName(arg);
169+
return folderName
170+
? new FileCancellationReceiverStrategy(folderName)
171+
: undefined;
172+
}
173+
174+
function extractCancellationFolderName(arg: string): string | undefined {
175+
const fileRegex = /^file:(.+)$/;
176+
const folderName = fileRegex.exec(arg);
177+
return folderName ? folderName[1] : undefined;
178+
}
179+
}
180+
181+
export class OperationCanceledException {}
182+
183+
export interface ICancellationToken {
184+
isCancellationRequested(): boolean;
185+
186+
/** @throws OperationCanceledException if isCancellationRequested is true */
187+
throwIfCancellationRequested(): void;
188+
}
189+
190+
/**
191+
* ThrottledCancellationToken taken from Typescript: https://github.com./microsoft/TypeScript/blob/79ffd03f8b73010fa03cef624e5f1770bc9c975b/src/services/services.ts#L1152
192+
*/
193+
export class ThrottledCancellationToken implements ICancellationToken {
194+
// Store when we last tried to cancel. Checking cancellation can be expensive (as we have
195+
// to marshall over to the host layer). So we only bother actually checking once enough
196+
// time has passed.
197+
private lastCancellationCheckTime = 0;
198+
199+
constructor(
200+
private cancellationToken: CancellationToken,
201+
private readonly throttleWaitMilliseconds = 20,
202+
) {}
203+
204+
public isCancellationRequested(): boolean {
205+
const time = performance.now();
206+
const duration = Math.abs(time - this.lastCancellationCheckTime);
207+
if (duration >= this.throttleWaitMilliseconds) {
208+
// Check no more than once every throttle wait milliseconds
209+
this.lastCancellationCheckTime = time;
210+
return this.cancellationToken.isCancellationRequested;
211+
}
212+
213+
return false;
214+
}
215+
216+
public throwIfCancellationRequested(): void {
217+
if (this.isCancellationRequested()) {
218+
throw new OperationCanceledException();
219+
}
220+
}
221+
}

src/capabilityCalculator.ts

+1-6
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import {
33
ServerCapabilities,
44
TextDocumentSyncKind,
55
} from "vscode-languageserver";
6-
import * as ElmAnalyseDiagnostics from "./providers/diagnostics/elmAnalyseDiagnostics";
76
import * as ElmMakeDiagnostics from "./providers/diagnostics/elmMakeDiagnostics";
87

98
export class CapabilityCalculator {
@@ -28,11 +27,7 @@ export class CapabilityCalculator {
2827
documentFormattingProvider: true,
2928
documentSymbolProvider: true,
3029
executeCommandProvider: {
31-
commands: [
32-
ElmAnalyseDiagnostics.CODE_ACTION_ELM_ANALYSE,
33-
ElmAnalyseDiagnostics.CODE_ACTION_ELM_ANALYSE_FIX_ALL,
34-
ElmMakeDiagnostics.CODE_ACTION_ELM_MAKE,
35-
],
30+
commands: [ElmMakeDiagnostics.CODE_ACTION_ELM_MAKE],
3631
},
3732
foldingRangeProvider: true,
3833
hoverProvider: true,

src/elmWorkspace.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import os from "os";
44
import path from "path";
55
import { container } from "tsyringe";
66
import util from "util";
7-
import { IConnection } from "vscode-languageserver";
7+
import { Connection } from "vscode-languageserver";
88
import { URI } from "vscode-uri";
99
import Parser, { Tree } from "web-tree-sitter";
1010
import { Forest, IForest } from "./forest";
@@ -55,7 +55,7 @@ export class ElmWorkspace implements IElmWorkspace {
5555
private elmFolders: IRootFolder[] = [];
5656
private forest: IForest = new Forest([]);
5757
private parser: Parser;
58-
private connection: IConnection;
58+
private connection: Connection;
5959
private settings: Settings;
6060
private typeCache: TypeCache;
6161
private typeChecker: TypeChecker | undefined;

src/index.ts

+9-5
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,14 @@ import * as Path from "path";
44
import "reflect-metadata";
55
import { container } from "tsyringe"; //must be after reflect-metadata
66
import {
7-
createConnection,
8-
IConnection,
7+
Connection,
98
InitializeParams,
109
InitializeResult,
1110
ProposedFeatures,
1211
} from "vscode-languageserver";
12+
import { createConnection } from "vscode-languageserver/node";
1313
import Parser from "web-tree-sitter";
14+
import { getCancellationStrategyFromArgv } from "./cancellation";
1415
import { CapabilityCalculator } from "./capabilityCalculator";
1516
import { ILanguageServer } from "./server";
1617
import { DocumentEvents } from "./util/documentEvents";
@@ -30,8 +31,10 @@ if (process.argv.length === 2) {
3031
}
3132

3233
// Composition root - be aware, there are some register calls that need to be done later
33-
container.register<IConnection>("Connection", {
34-
useValue: createConnection(ProposedFeatures.all),
34+
container.register<Connection>("Connection", {
35+
useValue: createConnection(ProposedFeatures.all, {
36+
cancellationStrategy: getCancellationStrategyFromArgv(process.argv),
37+
}),
3538
});
3639
container.registerSingleton<Parser>("Parser", Parser);
3740

@@ -40,7 +43,7 @@ container.register(TextDocumentEvents, {
4043
useValue: new TextDocumentEvents(),
4144
});
4245

43-
const connection = container.resolve<IConnection>("Connection");
46+
const connection = container.resolve<Connection>("Connection");
4447

4548
let server: ILanguageServer;
4649

@@ -62,6 +65,7 @@ connection.onInitialize(
6265
container.register(CapabilityCalculator, {
6366
useValue: new CapabilityCalculator(params.capabilities),
6467
});
68+
6569
const initializationOptions = params.initializationOptions ?? {};
6670

6771
container.register("Settings", {

src/module.ts

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * as Protocol from "./protocol";

0 commit comments

Comments
 (0)