diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts index b2a8bb3a9cc8c..36237a54de7ba 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { deepStrictEqual, doesNotThrow, equal, ok, strictEqual, throws } from 'assert'; -import { ConfigurationTarget, Disposable, env, EnvironmentVariableCollection, EnvironmentVariableMutator, EnvironmentVariableMutatorOptions, EnvironmentVariableMutatorType, EnvironmentVariableScope, EventEmitter, ExtensionContext, extensions, ExtensionTerminalOptions, Pseudoterminal, Terminal, TerminalDimensions, TerminalExitReason, TerminalOptions, TerminalState, UIKind, Uri, window, workspace } from 'vscode'; +import { commands, ConfigurationTarget, Disposable, env, EnvironmentVariableCollection, EnvironmentVariableMutator, EnvironmentVariableMutatorOptions, EnvironmentVariableMutatorType, EnvironmentVariableScope, EventEmitter, ExtensionContext, extensions, ExtensionTerminalOptions, Pseudoterminal, Terminal, TerminalDimensions, TerminalExitReason, TerminalOptions, TerminalState, UIKind, Uri, window, workspace } from 'vscode'; import { assertNoRpc, poll } from '../utils'; // Disable terminal tests: @@ -347,6 +347,45 @@ import { assertNoRpc, poll } from '../utils'; }); }); + suite('selection', () => { + test('should be undefined immediately after creation', async () => { + const terminal = window.createTerminal({ name: 'selection test' }); + terminal.show(); + equal(terminal.selection, undefined); + terminal.dispose(); + }); + test('should be defined after selecting all content', async () => { + const terminal = window.createTerminal({ name: 'selection test' }); + terminal.show(); + // Wait for some terminal data + await new Promise(r => { + const disposable = window.onDidWriteTerminalData(() => { + disposable.dispose(); + r(); + }); + }); + await commands.executeCommand('workbench.action.terminal.selectAll'); + await poll(() => Promise.resolve(), () => terminal.selection !== undefined, 'selection should be defined'); + terminal.dispose(); + }); + test('should be undefined after clearing a selection', async () => { + const terminal = window.createTerminal({ name: 'selection test' }); + terminal.show(); + // Wait for some terminal data + await new Promise(r => { + const disposable = window.onDidWriteTerminalData(() => { + disposable.dispose(); + r(); + }); + }); + await commands.executeCommand('workbench.action.terminal.selectAll'); + await poll(() => Promise.resolve(), () => terminal.selection !== undefined, 'selection should be defined'); + await commands.executeCommand('workbench.action.terminal.clearSelection'); + await poll(() => Promise.resolve(), () => terminal.selection === undefined, 'selection should not be defined'); + terminal.dispose(); + }); + }); + suite('window.onDidWriteTerminalData', () => { test('should listen to all future terminal data events', (done) => { const openEvents: string[] = []; diff --git a/src/vs/workbench/api/browser/mainThreadTerminalService.ts b/src/vs/workbench/api/browser/mainThreadTerminalService.ts index b2f7c9a7dd300..ffa8985978f8b 100644 --- a/src/vs/workbench/api/browser/mainThreadTerminalService.ts +++ b/src/vs/workbench/api/browser/mainThreadTerminalService.ts @@ -85,6 +85,7 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape this._store.add(_terminalService.onDidChangeActiveInstance(instance => this._onActiveTerminalChanged(instance ? instance.instanceId : null))); this._store.add(_terminalService.onDidChangeInstanceTitle(instance => instance && this._onTitleChanged(instance.instanceId, instance.title))); this._store.add(_terminalService.onDidInputInstanceData(instance => this._proxy.$acceptTerminalInteraction(instance.instanceId))); + this._store.add(_terminalService.onDidChangeSelection(instance => this._proxy.$acceptTerminalSelection(instance.instanceId, instance.selection))); // Set initial ext host state for (const instance of this._terminalService.instances) { diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index a9dabecd9dec0..f2c2bc7893fde 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -2012,6 +2012,7 @@ export interface ExtHostTerminalServiceShape { $acceptTerminalDimensions(id: number, cols: number, rows: number): void; $acceptTerminalMaximumDimensions(id: number, cols: number, rows: number): void; $acceptTerminalInteraction(id: number): void; + $acceptTerminalSelection(id: number, selection: string | undefined): void; $startExtensionTerminal(id: number, initialDimensions: ITerminalDimensionsDto | undefined): Promise; $acceptProcessAckDataEvent(id: number, charCount: number): void; $acceptProcessInput(id: number, data: string): void; diff --git a/src/vs/workbench/api/common/extHostTerminalService.ts b/src/vs/workbench/api/common/extHostTerminalService.ts index a6c7fd37e18b4..96e5d9671353b 100644 --- a/src/vs/workbench/api/common/extHostTerminalService.ts +++ b/src/vs/workbench/api/common/extHostTerminalService.ts @@ -75,6 +75,7 @@ export class ExtHostTerminal { private _rows: number | undefined; private _exitStatus: vscode.TerminalExitStatus | undefined; private _state: vscode.TerminalState = { isInteractedWith: false }; + private _selection: string | undefined; public isOpen: boolean = false; @@ -106,6 +107,9 @@ export class ExtHostTerminal { get state(): vscode.TerminalState { return that._state; }, + get selection(): string | undefined { + return that._selection; + }, sendText(text: string, addNewLine: boolean = true): void { that._checkDisposed(); that._proxy.$sendText(that._id, text, addNewLine); @@ -233,6 +237,10 @@ export class ExtHostTerminal { return false; } + public setSelection(selection: string | undefined): void { + this._selection = selection; + } + public _setProcessId(processId: number | undefined): void { // The event may fire 2 times when the panel is restored if (this._pidPromiseComplete) { @@ -615,6 +623,10 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I } } + public $acceptTerminalSelection(id: number, selection: string | undefined): void { + this._getTerminalById(id)?.setSelection(selection); + } + public $acceptProcessResize(id: number, cols: number, rows: number): void { try { this._terminalProcesses.get(id)?.resize(cols, rows); diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.ts b/src/vs/workbench/contrib/terminal/browser/terminal.ts index f57623f7ca81c..ec146c02cf6b1 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.ts @@ -176,6 +176,7 @@ export interface ITerminalService extends ITerminalInstanceHost { readonly onDidChangeInstanceColor: Event<{ instance: ITerminalInstance; userInitiated: boolean }>; readonly onDidChangeInstancePrimaryStatus: Event; readonly onDidInputInstanceData: Event; + readonly onDidChangeSelection: Event; readonly onDidRegisterProcessSupport: Event; readonly onDidChangeConnectionState: Event; @@ -546,6 +547,7 @@ export interface ITerminalInstance { onDidRequestFocus: Event; onDidBlur: Event; onDidInputData: Event; + onDidChangeSelection: Event; /** * An event that fires when a terminal is dropped on this instance via drag and drop. diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index 91e2db9339b82..f814059bd6d54 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -316,6 +316,8 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { readonly onDidBlur = this._onDidBlur.event; private readonly _onDidInputData = this._register(new Emitter()); readonly onDidInputData = this._onDidInputData.event; + private readonly _onDidChangeSelection = this._register(new Emitter()); + readonly onDidChangeSelection = this._onDidChangeSelection.event; private readonly _onRequestAddInstanceToGroup = this._register(new Emitter()); readonly onRequestAddInstanceToGroup = this._onRequestAddInstanceToGroup.event; private readonly _onDidChangeHasChildProcesses = this._register(new Emitter()); @@ -1722,6 +1724,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { } private async _onSelectionChange(): Promise { + this._onDidChangeSelection.fire(this); if (this._configurationService.getValue(TerminalSettingId.CopyOnSelection)) { if (this.hasSelection()) { await this.copySelection(); diff --git a/src/vs/workbench/contrib/terminal/browser/terminalService.ts b/src/vs/workbench/contrib/terminal/browser/terminalService.ts index b0e8a2c08cdc7..27ff2a40b233c 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalService.ts @@ -152,6 +152,8 @@ export class TerminalService implements ITerminalService { get onDidChangeInstancePrimaryStatus(): Event { return this._onDidChangeInstancePrimaryStatus.event; } private readonly _onDidInputInstanceData = new Emitter(); get onDidInputInstanceData(): Event { return this._onDidInputInstanceData.event; } + private readonly _onDidChangeSelection = new Emitter(); + get onDidChangeSelection(): Event { return this._onDidChangeSelection.event; } private readonly _onDidDisposeGroup = new Emitter(); get onDidDisposeGroup(): Event { return this._onDidDisposeGroup.event; } private readonly _onDidChangeGroups = new Emitter(); @@ -836,7 +838,8 @@ export class TerminalService implements ITerminalService { instance.onMaximumDimensionsChanged(() => this._onDidMaxiumumDimensionsChange.fire(instance)), instance.onDidInputData(this._onDidInputInstanceData.fire, this._onDidInputInstanceData), instance.onDidFocus(this._onDidChangeActiveInstance.fire, this._onDidChangeActiveInstance), - instance.onRequestAddInstanceToGroup(async e => await this._addInstanceToGroup(instance, e)) + instance.onRequestAddInstanceToGroup(async e => await this._addInstanceToGroup(instance, e)), + instance.onDidChangeSelection(this._onDidChangeSelection.fire, this._onDidChangeSelection) ]; instance.onDisposed(() => dispose(instanceDisposables)); } diff --git a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts index c251c3a399d09..cac9a0dfd298f 100644 --- a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts +++ b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts @@ -85,6 +85,7 @@ export const allApiProposals = Object.freeze({ terminalDataWriteEvent: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.terminalDataWriteEvent.d.ts', terminalDimensions: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.terminalDimensions.d.ts', terminalQuickFixProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.terminalQuickFixProvider.d.ts', + terminalSelection: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.terminalSelection.d.ts', testCoverage: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.testCoverage.d.ts', testObserver: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.testObserver.d.ts', textSearchProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.textSearchProvider.d.ts', diff --git a/src/vscode-dts/vscode.proposed.terminalSelection.d.ts b/src/vscode-dts/vscode.proposed.terminalSelection.d.ts new file mode 100644 index 0000000000000..706bcdafe104f --- /dev/null +++ b/src/vscode-dts/vscode.proposed.terminalSelection.d.ts @@ -0,0 +1,16 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + + // https://github.com/microsoft/vscode/issues/188173 + + export interface Terminal { + /** + * The selected text of the terminal or undefined if there is no selection. + */ + readonly selection: string | undefined; + } +}