Path: blob/main/extensions/copilot/test/simulation/language/tsServerClient.ts
13394 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/4import assert from 'assert';5import * as cp from 'child_process';6import * as fs from 'fs';7import path from 'path';8import ts from 'typescript/lib/tsserverlibrary';9import type * as vscode from 'vscode';10import { DeferredPromise } from '../../../src/util/vs/base/common/async';11import { Range } from '../../../src/vscodeTypes';12import { REPO_ROOT } from '../../base/stest';13import { doRunNpmInstall } from '../diagnosticProviders/tsc';14import { setupTemporaryWorkspace } from '../diagnosticProviders/utils';15import { cleanTempDirWithRetry, createTempDir } from '../stestUtil';1617class TSServerRPC {1819private _seq: number;2021private _awaitingResponse: Map<number /* seq id */, DeferredPromise<ts.server.protocol.Response>>;2223private _stdoutBuffer: string;2425constructor(26private readonly _server: cp.ChildProcess27) {28_server.stdin?.setDefaultEncoding('utf8');29_server.stdout?.setEncoding('utf8');30_server.stderr?.setEncoding('utf8');31_server.on('close', () => {32for (const reply of this._awaitingResponse.values()) {33reply.error(new Error('server closed'));34}35});3637this._seq = 0;38this._awaitingResponse = new Map();39this._stdoutBuffer = '';4041this._registerOnDataHandler();42}4344send(data: Omit<ts.server.protocol.Request, 'seq'>) {45const obj = { ...data, seq: this._seq++ };46const objS = `${JSON.stringify(obj)}\r\n`;47const reply = new DeferredPromise<ts.server.protocol.Response>();48this._server.stdin!.write(objS, err => {49if (err) {50reply.error(err);51}52});53this._awaitingResponse.set(obj.seq, reply);54return reply.p;55}5657emit(data: Omit<ts.server.protocol.Request, 'seq'>) {58const obj = { ...data, seq: this._seq++ };59const objS = `${JSON.stringify(obj)}\r\n`;60this._server.stdin!.write(objS, (_err) => {61// ignored, server closed62});63}6465private _registerOnDataHandler() {66this._server.stdout!.on('data', (chunk) => {67this._stdoutBuffer += chunk;68this._tryProcessStdoutBuffer();69});70this._server.stderr!.on('data', (chunk) => {71console.error(`stderr chunk: ${chunk}`);72});73}7475private _tryProcessStdoutBuffer() {76do {77const eolIndex = this._stdoutBuffer.indexOf('\r\n');78if (eolIndex === -1) {79break;80}8182// parse header83const firstLine = this._stdoutBuffer.substring(0, eolIndex);84const contentLength = parseInt(firstLine.substring('Content-Length: '.length), 10);8586// try parse body87const body = this._stdoutBuffer.substring(eolIndex + 4, eolIndex + 4 + contentLength);88if (body.length < contentLength) {89// entire body did not arrive yet90break;91}92this._stdoutBuffer = this._stdoutBuffer.substring(eolIndex + 4 + contentLength);9394this._handleServerMessage(JSON.parse(body) as ts.server.protocol.Message);95} while (true);96}9798private _handleServerMessage(msg: ts.server.protocol.Message) {99switch (msg.type) {100case 'event':101case 'request':102break;103case 'response': {104const resp = msg as ts.server.protocol.Response;105const respP = this._awaitingResponse.get(resp.request_seq);106if (respP === undefined) {107console.error(`received response for unexpected seq ${resp.request_seq}`);108} else {109respP.complete(resp);110}111break;112}113}114}115}116117type TSServerClientState =118| { k: 'uninitialized' }119| {120k: 'initialized';121workspacePath: string;122files: { filePath: string; fileName: string; fileContents: string }[];123tsServerCP: cp.ChildProcess;124tsServerRpc: TSServerRPC;125}126;127128export class TSServerClient {129130static readonly id = 'tsc-language-features';131132static cacheVersion(): number {133return 1;134}135136private _state: TSServerClientState;137private _initPromise: Promise<void> | undefined;138139constructor(private readonly _workspaceFiles: { fileName: string; fileContents: string }[]) {140this._state = { k: 'uninitialized' };141}142143private async _init() {144this._initPromise ??= (async () => {145const { workspacePath, files } = await this._setUp(this._workspaceFiles);146147const tsserverPath = path.resolve(path.join(REPO_ROOT, 'node_modules/typescript/lib/tsserver.js'));148const tsServerCP = cp.fork(tsserverPath, {149cwd: workspacePath,150stdio: ['pipe', 'pipe', 'pipe', 'ipc']151});152153const tsServerRpc = new TSServerRPC(tsServerCP);154155this._state = {156k: 'initialized',157workspacePath,158files,159tsServerCP,160tsServerRpc,161};162163// send "open" notifications164for (const file of files) {165tsServerRpc.emit({166'type': 'request',167'command': 'open',168'arguments': { 'file': file.filePath }169});170}171})();172173await this._initPromise;174}175176async teardown() {177if (this._state.k === 'uninitialized') {178return;179}180181await this._state.tsServerRpc.send({182'type': 'request',183'command': 'exit',184});185186await cleanTempDirWithRetry(this._state.workspacePath);187}188189async findDefinitions(fileName: string, position: vscode.Position): Promise<{ fileName: string; range: vscode.Range }[]> {190return this.find(ts.server.protocol.CommandTypes.DefinitionAndBoundSpan, fileName, position);191}192193async findReferences(fileName: string, position: vscode.Position): Promise<{ fileName: string; range: vscode.Range }[]> {194return this.find(ts.server.protocol.CommandTypes.References, fileName, position);195}196197async find(198command: ts.server.protocol.CommandTypes.References | ts.server.protocol.CommandTypes.DefinitionAndBoundSpan,199fileName: string, position: vscode.Position200): Promise<{ fileName: string; range: vscode.Range }[]> {201await this._init();202assert(this._state.k === 'initialized');203204const response = await this._state.tsServerRpc.send(205{206type: 'request',207command,208arguments: {209file: this._state.files.find(file => file.fileName === fileName)!.filePath,210line: position.line + 1,211offset: position.character + 1,212}213} satisfies Omit<ts.server.protocol.Request, 'seq'>214);215216assert(response.command === command);217218if (!response.success) {219throw new Error(`Request failed: ${response.message}`);220}221222const locations = command === ts.server.protocol.CommandTypes.DefinitionAndBoundSpan ? response.body.definitions : response.body.refs;223const workspacePathWithSlash = path.join(this._state.workspacePath, '/');224const resultingDefinitions = [];225226for (const location of locations) {227if (path.normalize(location.file).startsWith(workspacePathWithSlash)) {228const range = new Range(location.start.line - 1, location.start.offset - 1, location.end.line - 1, location.end.offset - 1);229const fileName = location.file.substring(workspacePathWithSlash.length);230resultingDefinitions.push({ fileName, range });231} else {232// ignore all matches in non-workspace files, e.g. in d.ts files233}234}235return resultingDefinitions;236}237238private async _setUp(_files: { fileName: string; fileContents: string }[] = []) {239const workspacePath = await createTempDir();240const files = await setupTemporaryWorkspace(workspacePath, _files);241242const packagejson = files.find(file => path.basename(file.fileName) === 'package.json');243if (packagejson) {244await doRunNpmInstall(path.dirname(packagejson.filePath));245}246247const hasTSConfigFile = files.some(file => path.basename(file.fileName) === 'tsconfig.json');248249if (!hasTSConfigFile) {250const tsconfigPath = path.join(workspacePath, 'tsconfig.json');251await fs.promises.writeFile(tsconfigPath, JSON.stringify({252'compilerOptions': {253'target': 'es2021',254'strict': true,255'module': 'commonjs',256'outDir': 'out',257'sourceMap': true258},259'exclude': [260'node_modules',261'outcome',262'scenarios'263]264}));265}266267return { workspacePath, files };268}269}270271272