Skip to content

Commit 6d19995

Browse files
committed
Implement new diagnostics system
* Add interruptable diagnostic provider * Enable type inference compiler diagnostics * Remove elm-analyse
1 parent 0ec4007 commit 6d19995

Some content is hidden

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

48 files changed

+1029
-823
lines changed

package-lock.json

Lines changed: 15 additions & 15 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
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

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

src/capabilityCalculator.ts

Lines changed: 1 addition & 6 deletions
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

Lines changed: 2 additions & 2 deletions
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";
@@ -52,7 +52,7 @@ export class ElmWorkspace implements IElmWorkspace {
5252
private elmFolders: IRootFolder[] = [];
5353
private forest: IForest = new Forest([]);
5454
private parser: Parser;
55-
private connection: IConnection;
55+
private connection: Connection;
5656
private settings: Settings;
5757
private typeCache: TypeCache;
5858
private typeChecker: TypeChecker | undefined;

src/index.ts

Lines changed: 10 additions & 6 deletions
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", {
@@ -86,6 +90,6 @@ connection.listen();
8690
// Don't die on unhandled Promise rejections
8791
process.on("unhandledRejection", (reason, p) => {
8892
connection.console.error(
89-
`Unhandled Rejection at: Promise ${p} reason:, ${reason}`,
93+
`Unhandleds Rejection at: Promise ${p} reason:, ${reason}`,
9094
);
9195
});

src/module.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * as Protocol from "./protocol";

0 commit comments

Comments
 (0)