Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 107 additions & 0 deletions client-node-tests/src/integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
this.events.push('start');
}

public override stop(): Promise<void> {
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 () => {
Expand Down
32 changes: 32 additions & 0 deletions client/src/common/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -858,6 +858,29 @@ export abstract class BaseLanguageClient implements FeatureClient<Middleware, La
return this._outputChannel;
}

protected isOutputChannelVisible(): boolean {
if (this._outputChannel === undefined) {
return false;
}
const outputChannelResource = getOutputChannelResourceName(this._outputChannel.name);
return this.getVisibleTextEditors().some(editor => {
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;
}
Expand Down Expand Up @@ -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 {
Expand Down
2 changes: 2 additions & 0 deletions client/src/node/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,7 @@ export class LanguageClient extends BaseLanguageClient {
}

public async restart(): Promise<void> {
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
Expand All @@ -247,6 +248,7 @@ export class LanguageClient extends BaseLanguageClient {
} else {
await this.start();
}
this.restoreOutputChannelVisibility(outputChannelVisible);
}

protected shutdown(mode: ShutdownMode, timeout: number = 2000): Promise<void> {
Expand Down