diff --git a/client-node-tests/src/integration.test.ts b/client-node-tests/src/integration.test.ts index 66a480803..a0dcb0fc9 100644 --- a/client-node-tests/src/integration.test.ts +++ b/client-node-tests/src/integration.test.ts @@ -242,6 +242,113 @@ suite('Server output', () => { }); }); +suite('Client restart', () => { + + test('Preserves output channel visibility after restart', async () => { + const client = new RestartTestLanguageClient('Restart', [ + createOutputTextEditor('output:ms-vscode.test-extension.Restart.log', 'Restart.log') + ]); + + await client.restart(); + + assert.deepStrictEqual(client.events, ['getVisibleTextEditors', 'stop', 'start', 'restoreOutputChannelVisibility:true']); + }); + + test('Does not restore hidden output channel after restart', async () => { + const client = new RestartTestLanguageClient('Restart', [ + createOutputTextEditor('output:ms-vscode.restart.Other.log', 'Other.log') + ]); + + await client.restart(); + + assert.deepStrictEqual(client.events, ['getVisibleTextEditors', 'stop', 'start', 'restoreOutputChannelVisibility:false']); + }); + + test('Detects visible output channel by normalized log channel id suffix', () => { + const client = new RestartTestLanguageClient('ESLint', [ + createOutputTextEditor('output:ms-vscode.test-extension.ESLint.log', 'ESLint.log') + ]); + + assert.strictEqual(client.isTestOutputChannelVisible(), true); + }); + + test('Does not match another visible output channel by substring', () => { + const client = new RestartTestLanguageClient('ESLint', [ + createOutputTextEditor('output:ms-vscode.eslint-extension.Other.log', 'Other.log') + ]); + + assert.strictEqual(client.isTestOutputChannelVisible(), false); + }); + + test('Detects output channels with VS Code sanitized log file names', () => { + const client = new RestartTestLanguageClient('C/C++', [ + createOutputTextEditor('output:ms-vscode.test-extension.CC++.log', 'CC++.log') + ]); + + assert.strictEqual(client.isTestOutputChannelVisible(), true); + }); +}); + +class RestartTestLanguageClient extends lsclient.LanguageClient { + + public readonly events: string[] = []; + + public constructor(outputChannelName: string, private readonly visibleTextEditors: readonly vscode.TextEditor[]) { + super('test restart', { module: 'unused', transport: lsclient.TransportKind.ipc }, { outputChannel: createLogOutputChannel(outputChannelName) }); + } + + public isTestOutputChannelVisible(): boolean { + return this.isOutputChannelVisible(); + } + + public override async start(): Promise { + this.events.push('start'); + } + + public override stop(): Promise { + this.events.push('stop'); + return Promise.resolve(); + } + + protected override getVisibleTextEditors(): readonly vscode.TextEditor[] { + this.events.push('getVisibleTextEditors'); + return this.visibleTextEditors; + } + + protected override restoreOutputChannelVisibility(wasVisible: boolean): void { + this.events.push(`restoreOutputChannelVisibility:${wasVisible}`); + } +} + +function createOutputTextEditor(uri: string, fileName: string): vscode.TextEditor { + return { + document: { + uri: vscode.Uri.parse(uri), + fileName + } + } as vscode.TextEditor; +} + +function createLogOutputChannel(name: string): vscode.LogOutputChannel { + return { + name, + append: () => undefined, + appendLine: () => undefined, + replace: () => undefined, + clear: () => undefined, + show: () => undefined, + hide: () => undefined, + dispose: () => undefined, + logLevel: vscode.LogLevel.Info, + onDidChangeLogLevel: () => ({ dispose: () => undefined }), + trace: () => undefined, + debug: () => undefined, + info: () => undefined, + warn: () => undefined, + error: () => undefined + } as vscode.LogOutputChannel; +} + suite('Socket transport', () => { test('Uses an OS-assigned port when transport.port is 0', async () => { diff --git a/client/src/common/client.ts b/client/src/common/client.ts index a5953c6ab..8d20619e0 100644 --- a/client/src/common/client.ts +++ b/client/src/common/client.ts @@ -858,6 +858,29 @@ export abstract class BaseLanguageClient implements FeatureClient { + if (editor.document.uri.scheme !== 'output') { + return false; + } + return matchesOutputChannelResource(editor.document.uri.toString(true), outputChannelResource) || matchesOutputChannelResource(editor.document.fileName, outputChannelResource); + }); + } + + protected getVisibleTextEditors(): readonly TextEditor[] { + return Window.visibleTextEditors; + } + + protected restoreOutputChannelVisibility(wasVisible: boolean): void { + if (wasVisible) { + this.outputChannel.show(true); + } + } + public get traceOutputChannel(): LogOutputChannel { return this._traceOutputChannel ? this._traceOutputChannel : this.outputChannel; } @@ -2548,6 +2571,15 @@ function createConnection(input: MessageReader, output: MessageWriter, errorHand return result; } +function getOutputChannelResourceName(name: string): string { + return `${name.replace(/[\\/:\*\?"<>\|]/g, '')}.log`.toLowerCase(); +} + +function matchesOutputChannelResource(resource: string, outputChannelResource: string): boolean { + const normalizedResource = resource.replace(/\\/g, '/').toLowerCase(); + return normalizedResource === outputChannelResource || normalizedResource.endsWith(`/${outputChannelResource}`) || normalizedResource.endsWith(`.${outputChannelResource}`); +} + // Exporting proposed protocol. export namespace ProposedFeatures { diff --git a/client/src/node/main.ts b/client/src/node/main.ts index 585d501bd..89a218921 100644 --- a/client/src/node/main.ts +++ b/client/src/node/main.ts @@ -236,6 +236,7 @@ export class LanguageClient extends BaseLanguageClient { } public async restart(): Promise { + const outputChannelVisible = this.isOutputChannelVisible(); await this.stop(); // We are in debug mode. Wait a little before we restart // so that the debug port can be freed. We can safely ignore @@ -247,6 +248,7 @@ export class LanguageClient extends BaseLanguageClient { } else { await this.start(); } + this.restoreOutputChannelVisibility(outputChannelVisible); } protected shutdown(mode: ShutdownMode, timeout: number = 2000): Promise {