Skip to content

Commit 8725b9b

Browse files
authored
feat: support communicating with tsserver using IPC (typescript-language-server#585)
1 parent 899ba6b commit 8725b9b

15 files changed

+1368
-271
lines changed

LICENSE

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,42 @@
1+
Parts of the code copied from the https://github.com/microsoft/vscode repository are licensed under the following license:
2+
3+
BEGIN LICENSE ----------------------------------------------------------------
4+
5+
MIT License
6+
7+
Copyright (c) 2015 - present Microsoft Corporation
8+
9+
Permission is hereby granted, free of charge, to any person obtaining a copy
10+
of this software and associated documentation files (the "Software"), to deal
11+
in the Software without restriction, including without limitation the rights
12+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13+
copies of the Software, and to permit persons to whom the Software is
14+
furnished to do so, subject to the following conditions:
15+
16+
The above copyright notice and this permission notice shall be included in all
17+
copies or substantial portions of the Software.
18+
19+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25+
SOFTWARE.
26+
27+
END LICESE -------------------------------------------------------------------
28+
29+
This applies to files that include the following license header:
30+
31+
/*---------------------------------------------------------------------------------------------
32+
* Copyright (c) Microsoft Corporation. All rights reserved.
33+
* Licensed under the MIT License. See License.txt in the project root for license information.
34+
*--------------------------------------------------------------------------------------------*/
35+
36+
37+
The rest of the code licensed under:
38+
39+
140
Apache License
241
Version 2.0, January 2004
342
http://www.apache.org/licenses/

src/lsp-server.spec.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,8 @@ describe('completion', () => {
158158
});
159159
const pos = position(doc, 'foo');
160160
const proposals = await server.completion({ textDocument: doc, position: pos });
161-
assert.isNull(proposals);
161+
assert.isNotNull(proposals);
162+
assert.strictEqual(proposals?.items.length, 0);
162163
server.didCloseTextDocument({ textDocument: doc });
163164
});
164165

src/lsp-server.ts

Lines changed: 4 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -34,52 +34,13 @@ import { IServerOptions } from './utils/configuration.js';
3434
import { TypeScriptAutoFixProvider } from './features/fix-all.js';
3535
import { TypeScriptInlayHintsProvider } from './features/inlay-hints.js';
3636
import { SourceDefinitionCommand } from './features/source-definition.js';
37-
import { LspClient } from './lsp-client.js';
3837
import { TypeScriptVersion, TypeScriptVersionProvider } from './tsServer/versionProvider.js';
3938
import { Position, Range } from './utils/typeConverters.js';
4039
import { CodeActionKind } from './utils/types.js';
4140
import { ConfigurationManager } from './configuration-manager.js';
4241

43-
class ServerInitializingIndicator {
44-
private _loadingProjectName?: string;
45-
private _progressReporter?: lsp.WorkDoneProgressReporter;
46-
47-
constructor(private lspClient: LspClient) {}
48-
49-
public reset(): void {
50-
if (this._loadingProjectName) {
51-
this._loadingProjectName = undefined;
52-
if (this._progressReporter) {
53-
this._progressReporter.done();
54-
this._progressReporter = undefined;
55-
}
56-
}
57-
}
58-
59-
public async startedLoadingProject(projectName: string): Promise<void> {
60-
// TS projects are loaded sequentially. Cancel existing task because it should always be resolved before
61-
// the incoming project loading task is.
62-
this.reset();
63-
64-
this._loadingProjectName = projectName;
65-
this._progressReporter = await this.lspClient.createProgressReporter();
66-
this._progressReporter.begin('Initializing JS/TS language features…');
67-
}
68-
69-
public finishedLoadingProject(projectName: string): void {
70-
if (this._loadingProjectName === projectName) {
71-
this._loadingProjectName = undefined;
72-
if (this._progressReporter) {
73-
this._progressReporter.done();
74-
this._progressReporter = undefined;
75-
}
76-
}
77-
}
78-
}
79-
8042
export class LspServer {
8143
private _tspClient: TspClient | null = null;
82-
private _loadingIndicator: ServerInitializingIndicator | null = null;
8344
private initializeParams: TypeScriptInitializeParams | null = null;
8445
private diagnosticQueue?: DiagnosticEventQueue;
8546
private configurationManager: ConfigurationManager;
@@ -106,10 +67,6 @@ export class LspServer {
10667
this._tspClient.shutdown();
10768
this._tspClient = null;
10869
}
109-
if (this._loadingIndicator) {
110-
this._loadingIndicator.reset();
111-
this._loadingIndicator = null;
112-
}
11370
}
11471

11572
private get tspClient(): TspClient {
@@ -119,13 +76,6 @@ export class LspServer {
11976
return this._tspClient;
12077
}
12178

122-
private get loadingIndicator(): ServerInitializingIndicator {
123-
if (!this._loadingIndicator) {
124-
throw new Error('Loading indicator not created. Did you forget to send the "initialize" request?');
125-
}
126-
return this._loadingIndicator;
127-
}
128-
12979
private findTypescriptVersion(): TypeScriptVersion | null {
13080
const typescriptVersionProvider = new TypeScriptVersionProvider(this.options, this.logger);
13181
// User-provided tsserver path.
@@ -158,7 +108,6 @@ export class LspServer {
158108
}
159109
this.initializeParams = params;
160110
const clientCapabilities = this.initializeParams.capabilities;
161-
this._loadingIndicator = new ServerInitializingIndicator(this.options.lspClient);
162111
this.workspaceRoot = this.initializeParams.rootUri ? uriToPath(this.initializeParams.rootUri) : this.initializeParams.rootPath || undefined;
163112

164113
const userInitializationOptions: TypeScriptInitializationOptions = this.initializeParams.initializationOptions || {};
@@ -218,8 +167,8 @@ export class LspServer {
218167
this.logger,
219168
);
220169
this._tspClient = new TspClient({
221-
apiVersion: typescriptVersion.version || API.defaultVersion,
222-
tsserverPath: typescriptVersion.tsServerPath,
170+
lspClient: this.options.lspClient,
171+
typescriptVersion,
223172
logFile,
224173
logVerbosity,
225174
disableAutomaticTypingAcquisition,
@@ -400,7 +349,7 @@ export class LspServer {
400349

401350
const { files } = this.documents;
402351
try {
403-
return await this.tspClient.request(CommandTypes.Geterr, { delay: 0, files }, this.diagnosticsTokenSource.token);
352+
return await this.tspClient.requestGeterr({ delay: 0, files }, this.diagnosticsTokenSource.token);
404353
} finally {
405354
if (this.diagnosticsTokenSource === geterrTokenSource) {
406355
this.diagnosticsTokenSource = undefined;
@@ -1176,18 +1125,8 @@ export class LspServer {
11761125
}
11771126

11781127
protected async onTsEvent(event: protocol.Event): Promise<void> {
1179-
if (event.event === EventTypes.SementicDiag ||
1180-
event.event === EventTypes.SyntaxDiag ||
1181-
event.event === EventTypes.SuggestionDiag) {
1128+
if (event.event === EventTypes.SementicDiag || event.event === EventTypes.SyntaxDiag || event.event === EventTypes.SuggestionDiag) {
11821129
this.diagnosticQueue?.updateDiagnostics(event.event, event as tsp.DiagnosticEvent);
1183-
} else if (event.event === EventTypes.ProjectLoadingStart) {
1184-
await this.loadingIndicator.startedLoadingProject((event as tsp.ProjectLoadingStartEvent).body.projectName);
1185-
} else if (event.event === EventTypes.ProjectLoadingFinish) {
1186-
this.loadingIndicator.finishedLoadingProject((event as tsp.ProjectLoadingFinishEvent).body.projectName);
1187-
} else {
1188-
this.logger.log('Ignored event', {
1189-
event: event.event,
1190-
});
11911130
}
11921131
}
11931132

src/test-utils.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ export function toPlatformEOL(text: string): string {
106106
return text;
107107
}
108108

109-
class TestLspClient implements LspClient {
109+
export class TestLspClient implements LspClient {
110110
private workspaceEditsListener: ((args: lsp.ApplyWorkspaceEditParams) => void) | null = null;
111111

112112
constructor(protected options: TestLspServerOptions, protected logger: ConsoleLogger) {}
@@ -125,11 +125,11 @@ class TestLspClient implements LspClient {
125125
return await task(progress);
126126
}
127127

128-
publishDiagnostics(args: lsp.PublishDiagnosticsParams) {
128+
publishDiagnostics(args: lsp.PublishDiagnosticsParams): void {
129129
return this.options.publishDiagnostics(args);
130130
}
131131

132-
showErrorMessage(message: string) {
132+
showErrorMessage(message: string): void {
133133
this.logger.error(`[showErrorMessage] ${message}`);
134134
}
135135

@@ -148,7 +148,7 @@ class TestLspClient implements LspClient {
148148
return { applied: true };
149149
}
150150

151-
async rename() {
151+
rename(): Promise<void> {
152152
throw new Error('unsupported');
153153
}
154154
}

src/tsServer/callbackMap.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
/*
6+
* Copyright (C) 2022 TypeFox and others.
7+
*
8+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License.
9+
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
10+
*/
11+
12+
import type tsp from 'typescript/lib/protocol.d.js';
13+
import { ServerResponse } from './requests.js';
14+
15+
export interface CallbackItem<R> {
16+
readonly onSuccess: (value: R) => void;
17+
readonly onError: (err: Error) => void;
18+
readonly queuingStartTime: number;
19+
readonly isAsync: boolean;
20+
}
21+
22+
export class CallbackMap<R extends tsp.Response> {
23+
private readonly _callbacks = new Map<number, CallbackItem<ServerResponse.Response<R> | undefined>>();
24+
private readonly _asyncCallbacks = new Map<number, CallbackItem<ServerResponse.Response<R> | undefined>>();
25+
26+
public destroy(cause: string): void {
27+
const cancellation = new ServerResponse.Cancelled(cause);
28+
for (const callback of this._callbacks.values()) {
29+
callback.onSuccess(cancellation);
30+
}
31+
this._callbacks.clear();
32+
for (const callback of this._asyncCallbacks.values()) {
33+
callback.onSuccess(cancellation);
34+
}
35+
this._asyncCallbacks.clear();
36+
}
37+
38+
public add(seq: number, callback: CallbackItem<ServerResponse.Response<R> | undefined>, isAsync: boolean): void {
39+
if (isAsync) {
40+
this._asyncCallbacks.set(seq, callback);
41+
} else {
42+
this._callbacks.set(seq, callback);
43+
}
44+
}
45+
46+
public fetch(seq: number): CallbackItem<ServerResponse.Response<R> | undefined> | undefined {
47+
const callback = this._callbacks.get(seq) || this._asyncCallbacks.get(seq);
48+
this.delete(seq);
49+
return callback;
50+
}
51+
52+
private delete(seq: number) {
53+
if (!this._callbacks.delete(seq)) {
54+
this._asyncCallbacks.delete(seq);
55+
}
56+
}
57+
}

src/tsServer/cancellation.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
/*
6+
* Copyright (C) 2022 TypeFox and others.
7+
*
8+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License.
9+
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
10+
*/
11+
12+
import fs from 'node:fs';
13+
import { temporaryFile } from 'tempy';
14+
// import Tracer from '../utils/tracer';
15+
16+
export interface OngoingRequestCanceller {
17+
readonly cancellationPipeName: string | undefined;
18+
tryCancelOngoingRequest(seq: number): boolean;
19+
}
20+
21+
export interface OngoingRequestCancellerFactory {
22+
create(/*serverId: string, tracer: Tracer*/): OngoingRequestCanceller;
23+
}
24+
25+
const noopRequestCanceller = new class implements OngoingRequestCanceller {
26+
public readonly cancellationPipeName = undefined;
27+
28+
public tryCancelOngoingRequest(_seq: number): boolean {
29+
return false;
30+
}
31+
};
32+
33+
export const noopRequestCancellerFactory = new class implements OngoingRequestCancellerFactory {
34+
create(/*_serverId: string, _tracer: Tracer*/): OngoingRequestCanceller {
35+
return noopRequestCanceller;
36+
}
37+
};
38+
39+
export class NodeRequestCanceller implements OngoingRequestCanceller {
40+
public readonly cancellationPipeName: string;
41+
42+
public constructor(
43+
// private readonly _serverId: string,
44+
// private readonly _tracer: Tracer,
45+
) {
46+
this.cancellationPipeName = temporaryFile({ name: 'tscancellation' });
47+
}
48+
49+
public tryCancelOngoingRequest(seq: number): boolean {
50+
if (!this.cancellationPipeName) {
51+
return false;
52+
}
53+
// this._tracer.logTrace(this._serverId, `TypeScript Server: trying to cancel ongoing request with sequence number ${seq}`);
54+
try {
55+
fs.writeFileSync(this.cancellationPipeName + String(seq), '');
56+
} catch {
57+
// noop
58+
}
59+
return true;
60+
}
61+
}
62+
63+
export const nodeRequestCancellerFactory = new class implements OngoingRequestCancellerFactory {
64+
create(/*serverId: string, tracer: Tracer*/): OngoingRequestCanceller {
65+
return new NodeRequestCanceller(/*serverId, tracer*/);
66+
}
67+
};

0 commit comments

Comments
 (0)