Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/test/simulation/language/tsServerClient.ts
13394 views
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
import assert from 'assert';
6
import * as cp from 'child_process';
7
import * as fs from 'fs';
8
import path from 'path';
9
import ts from 'typescript/lib/tsserverlibrary';
10
import type * as vscode from 'vscode';
11
import { DeferredPromise } from '../../../src/util/vs/base/common/async';
12
import { Range } from '../../../src/vscodeTypes';
13
import { REPO_ROOT } from '../../base/stest';
14
import { doRunNpmInstall } from '../diagnosticProviders/tsc';
15
import { setupTemporaryWorkspace } from '../diagnosticProviders/utils';
16
import { cleanTempDirWithRetry, createTempDir } from '../stestUtil';
17
18
class TSServerRPC {
19
20
private _seq: number;
21
22
private _awaitingResponse: Map<number /* seq id */, DeferredPromise<ts.server.protocol.Response>>;
23
24
private _stdoutBuffer: string;
25
26
constructor(
27
private readonly _server: cp.ChildProcess
28
) {
29
_server.stdin?.setDefaultEncoding('utf8');
30
_server.stdout?.setEncoding('utf8');
31
_server.stderr?.setEncoding('utf8');
32
_server.on('close', () => {
33
for (const reply of this._awaitingResponse.values()) {
34
reply.error(new Error('server closed'));
35
}
36
});
37
38
this._seq = 0;
39
this._awaitingResponse = new Map();
40
this._stdoutBuffer = '';
41
42
this._registerOnDataHandler();
43
}
44
45
send(data: Omit<ts.server.protocol.Request, 'seq'>) {
46
const obj = { ...data, seq: this._seq++ };
47
const objS = `${JSON.stringify(obj)}\r\n`;
48
const reply = new DeferredPromise<ts.server.protocol.Response>();
49
this._server.stdin!.write(objS, err => {
50
if (err) {
51
reply.error(err);
52
}
53
});
54
this._awaitingResponse.set(obj.seq, reply);
55
return reply.p;
56
}
57
58
emit(data: Omit<ts.server.protocol.Request, 'seq'>) {
59
const obj = { ...data, seq: this._seq++ };
60
const objS = `${JSON.stringify(obj)}\r\n`;
61
this._server.stdin!.write(objS, (_err) => {
62
// ignored, server closed
63
});
64
}
65
66
private _registerOnDataHandler() {
67
this._server.stdout!.on('data', (chunk) => {
68
this._stdoutBuffer += chunk;
69
this._tryProcessStdoutBuffer();
70
});
71
this._server.stderr!.on('data', (chunk) => {
72
console.error(`stderr chunk: ${chunk}`);
73
});
74
}
75
76
private _tryProcessStdoutBuffer() {
77
do {
78
const eolIndex = this._stdoutBuffer.indexOf('\r\n');
79
if (eolIndex === -1) {
80
break;
81
}
82
83
// parse header
84
const firstLine = this._stdoutBuffer.substring(0, eolIndex);
85
const contentLength = parseInt(firstLine.substring('Content-Length: '.length), 10);
86
87
// try parse body
88
const body = this._stdoutBuffer.substring(eolIndex + 4, eolIndex + 4 + contentLength);
89
if (body.length < contentLength) {
90
// entire body did not arrive yet
91
break;
92
}
93
this._stdoutBuffer = this._stdoutBuffer.substring(eolIndex + 4 + contentLength);
94
95
this._handleServerMessage(JSON.parse(body) as ts.server.protocol.Message);
96
} while (true);
97
}
98
99
private _handleServerMessage(msg: ts.server.protocol.Message) {
100
switch (msg.type) {
101
case 'event':
102
case 'request':
103
break;
104
case 'response': {
105
const resp = msg as ts.server.protocol.Response;
106
const respP = this._awaitingResponse.get(resp.request_seq);
107
if (respP === undefined) {
108
console.error(`received response for unexpected seq ${resp.request_seq}`);
109
} else {
110
respP.complete(resp);
111
}
112
break;
113
}
114
}
115
}
116
}
117
118
type TSServerClientState =
119
| { k: 'uninitialized' }
120
| {
121
k: 'initialized';
122
workspacePath: string;
123
files: { filePath: string; fileName: string; fileContents: string }[];
124
tsServerCP: cp.ChildProcess;
125
tsServerRpc: TSServerRPC;
126
}
127
;
128
129
export class TSServerClient {
130
131
static readonly id = 'tsc-language-features';
132
133
static cacheVersion(): number {
134
return 1;
135
}
136
137
private _state: TSServerClientState;
138
private _initPromise: Promise<void> | undefined;
139
140
constructor(private readonly _workspaceFiles: { fileName: string; fileContents: string }[]) {
141
this._state = { k: 'uninitialized' };
142
}
143
144
private async _init() {
145
this._initPromise ??= (async () => {
146
const { workspacePath, files } = await this._setUp(this._workspaceFiles);
147
148
const tsserverPath = path.resolve(path.join(REPO_ROOT, 'node_modules/typescript/lib/tsserver.js'));
149
const tsServerCP = cp.fork(tsserverPath, {
150
cwd: workspacePath,
151
stdio: ['pipe', 'pipe', 'pipe', 'ipc']
152
});
153
154
const tsServerRpc = new TSServerRPC(tsServerCP);
155
156
this._state = {
157
k: 'initialized',
158
workspacePath,
159
files,
160
tsServerCP,
161
tsServerRpc,
162
};
163
164
// send "open" notifications
165
for (const file of files) {
166
tsServerRpc.emit({
167
'type': 'request',
168
'command': 'open',
169
'arguments': { 'file': file.filePath }
170
});
171
}
172
})();
173
174
await this._initPromise;
175
}
176
177
async teardown() {
178
if (this._state.k === 'uninitialized') {
179
return;
180
}
181
182
await this._state.tsServerRpc.send({
183
'type': 'request',
184
'command': 'exit',
185
});
186
187
await cleanTempDirWithRetry(this._state.workspacePath);
188
}
189
190
async findDefinitions(fileName: string, position: vscode.Position): Promise<{ fileName: string; range: vscode.Range }[]> {
191
return this.find(ts.server.protocol.CommandTypes.DefinitionAndBoundSpan, fileName, position);
192
}
193
194
async findReferences(fileName: string, position: vscode.Position): Promise<{ fileName: string; range: vscode.Range }[]> {
195
return this.find(ts.server.protocol.CommandTypes.References, fileName, position);
196
}
197
198
async find(
199
command: ts.server.protocol.CommandTypes.References | ts.server.protocol.CommandTypes.DefinitionAndBoundSpan,
200
fileName: string, position: vscode.Position
201
): Promise<{ fileName: string; range: vscode.Range }[]> {
202
await this._init();
203
assert(this._state.k === 'initialized');
204
205
const response = await this._state.tsServerRpc.send(
206
{
207
type: 'request',
208
command,
209
arguments: {
210
file: this._state.files.find(file => file.fileName === fileName)!.filePath,
211
line: position.line + 1,
212
offset: position.character + 1,
213
}
214
} satisfies Omit<ts.server.protocol.Request, 'seq'>
215
);
216
217
assert(response.command === command);
218
219
if (!response.success) {
220
throw new Error(`Request failed: ${response.message}`);
221
}
222
223
const locations = command === ts.server.protocol.CommandTypes.DefinitionAndBoundSpan ? response.body.definitions : response.body.refs;
224
const workspacePathWithSlash = path.join(this._state.workspacePath, '/');
225
const resultingDefinitions = [];
226
227
for (const location of locations) {
228
if (path.normalize(location.file).startsWith(workspacePathWithSlash)) {
229
const range = new Range(location.start.line - 1, location.start.offset - 1, location.end.line - 1, location.end.offset - 1);
230
const fileName = location.file.substring(workspacePathWithSlash.length);
231
resultingDefinitions.push({ fileName, range });
232
} else {
233
// ignore all matches in non-workspace files, e.g. in d.ts files
234
}
235
}
236
return resultingDefinitions;
237
}
238
239
private async _setUp(_files: { fileName: string; fileContents: string }[] = []) {
240
const workspacePath = await createTempDir();
241
const files = await setupTemporaryWorkspace(workspacePath, _files);
242
243
const packagejson = files.find(file => path.basename(file.fileName) === 'package.json');
244
if (packagejson) {
245
await doRunNpmInstall(path.dirname(packagejson.filePath));
246
}
247
248
const hasTSConfigFile = files.some(file => path.basename(file.fileName) === 'tsconfig.json');
249
250
if (!hasTSConfigFile) {
251
const tsconfigPath = path.join(workspacePath, 'tsconfig.json');
252
await fs.promises.writeFile(tsconfigPath, JSON.stringify({
253
'compilerOptions': {
254
'target': 'es2021',
255
'strict': true,
256
'module': 'commonjs',
257
'outDir': 'out',
258
'sourceMap': true
259
},
260
'exclude': [
261
'node_modules',
262
'outcome',
263
'scenarios'
264
]
265
}));
266
}
267
268
return { workspacePath, files };
269
}
270
}
271
272