From d4d42e166791361434f8c88cb78e79e05cfe1348 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Thu, 25 May 2023 15:21:03 -0700 Subject: [PATCH 01/10] wip --- .../workbench/workbench-dev.html | 1 + .../electron-sandbox/workbench/workbench.html | 1 + .../common/capabilities/capabilities.ts | 8 ++ .../terminal/browser/media/scrollbar.css | 11 ++ .../terminal/browser/media/terminal.css | 8 ++ .../contrib/terminal/browser/terminal.ts | 38 ++++- .../terminal/browser/terminalInstance.ts | 86 +++++------- .../terminal/browser/terminalService.ts | 28 +++- .../terminal/browser/xterm/xtermTerminal.ts | 60 ++++++-- .../contrib/testing/browser/media/testing.css | 4 +- .../testing/browser/testingOutputPeek.ts | 130 +++++++++++++----- 11 files changed, 272 insertions(+), 103 deletions(-) diff --git a/src/vs/code/electron-sandbox/workbench/workbench-dev.html b/src/vs/code/electron-sandbox/workbench/workbench-dev.html index 42fbfd69ac313..421db9aa19ca4 100644 --- a/src/vs/code/electron-sandbox/workbench/workbench-dev.html +++ b/src/vs/code/electron-sandbox/workbench/workbench-dev.html @@ -57,6 +57,7 @@ notebookRenderer stickyScrollViewLayer tokenizeToString + outputTerminalData ; "/> diff --git a/src/vs/code/electron-sandbox/workbench/workbench.html b/src/vs/code/electron-sandbox/workbench/workbench.html index 9096f5261fb46..aca0a24d44206 100644 --- a/src/vs/code/electron-sandbox/workbench/workbench.html +++ b/src/vs/code/electron-sandbox/workbench/workbench.html @@ -57,6 +57,7 @@ notebookRenderer stickyScrollViewLayer tokenizeToString + outputTerminalData ; "/> diff --git a/src/vs/platform/terminal/common/capabilities/capabilities.ts b/src/vs/platform/terminal/common/capabilities/capabilities.ts index 552b181163966..fb46fd94a56dd 100644 --- a/src/vs/platform/terminal/common/capabilities/capabilities.ts +++ b/src/vs/platform/terminal/common/capabilities/capabilities.ts @@ -102,6 +102,14 @@ export interface ITerminalCapabilityStore { get(capability: T): ITerminalCapabilityImplMap[T] | undefined; } +export const emptyTerminalCapabilityStore: ITerminalCapabilityStore = { + items: [][Symbol.iterator](), + onDidAddCapability: Event.None, + onDidRemoveCapability: Event.None, + has: () => false, + get: () => undefined +}; + /** * Maps capability types to their implementation, enabling strongly typed fetching of * implementations. diff --git a/src/vs/workbench/contrib/terminal/browser/media/scrollbar.css b/src/vs/workbench/contrib/terminal/browser/media/scrollbar.css index d87b418ed8977..b267692727c66 100644 --- a/src/vs/workbench/contrib/terminal/browser/media/scrollbar.css +++ b/src/vs/workbench/contrib/terminal/browser/media/scrollbar.css @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +.xterm-detached-instance .xterm-viewport, .monaco-workbench .editor-instance .xterm-viewport, .monaco-workbench .pane-body.integrated-terminal .xterm-viewport { /* Use the hack presented in https://stackoverflow.com/a/38748186/1156119 to get opacity transitions working on the scrollbar */ @@ -12,44 +13,54 @@ transition: background-color 800ms linear; } +.xterm-detached-instance .xterm-viewport, .monaco-workbench .editor-instance .xterm-viewport, .monaco-workbench .pane-body.integrated-terminal .xterm-viewport { scrollbar-width: thin; } +.xterm-detached-instance .xterm-viewport::-webkit-scrollbar, .monaco-workbench .editor-instance .xterm-viewport::-webkit-scrollbar, .monaco-workbench .pane-body.integrated-terminal .xterm-viewport::-webkit-scrollbar { width: 10px; } +.xterm-detached-instance .xterm-viewport::-webkit-scrollbar-track, .monaco-workbench .editor-instance .xterm-viewport::-webkit-scrollbar-track, .monaco-workbench .pane-body.integrated-terminal .xterm-viewport::-webkit-scrollbar-track { opacity: 0; } +.xterm-detached-instance .xterm-viewport::-webkit-scrollbar-thumb, .monaco-workbench .editor-instance .xterm-viewport::-webkit-scrollbar-thumb, .monaco-workbench .pane-body.integrated-terminal .xterm-viewport::-webkit-scrollbar-thumb { min-height: 20px; background-color: inherit; } +.xterm-detached-instance .force-scrollbar .xterm .xterm-viewport, .monaco-workbench .editor-instance .force-scrollbar .xterm .xterm-viewport, .monaco-workbench .pane-body.integrated-terminal .force-scrollbar .xterm .xterm-viewport, +.xterm-detached-instance .xterm.focus .xterm-viewport, .monaco-workbench .editor-instance .xterm.focus .xterm-viewport, .monaco-workbench .pane-body.integrated-terminal .xterm.focus .xterm-viewport, +.xterm-detached-instance .xterm:focus .xterm-viewport, .monaco-workbench .editor-instance .xterm:focus .xterm-viewport, .monaco-workbench .pane-body.integrated-terminal .xterm:focus .xterm-viewport, +.xterm-detached-instance .xterm:hover .xterm-viewport, .monaco-workbench .editor-instance .xterm:hover .xterm-viewport, .monaco-workbench .pane-body.integrated-terminal .xterm:hover .xterm-viewport { transition: opacity 100ms linear; cursor: default; } +.xterm-detached-instance .xterm .xterm-viewport::-webkit-scrollbar-thumb:hover, .monaco-workbench .editor-instance .xterm .xterm-viewport::-webkit-scrollbar-thumb:hover, .monaco-workbench .pane-body.integrated-terminal .xterm .xterm-viewport::-webkit-scrollbar-thumb:hover { transition: opacity 0ms linear; } +.xterm-detached-instance .xterm .xterm-viewport::-webkit-scrollbar-thumb:window-inactive, .monaco-workbench .editor-instance .xterm .xterm-viewport::-webkit-scrollbar-thumb:window-inactive, .monaco-workbench .pane-body.integrated-terminal .xterm .xterm-viewport::-webkit-scrollbar-thumb:window-inactive { background-color: inherit; diff --git a/src/vs/workbench/contrib/terminal/browser/media/terminal.css b/src/vs/workbench/contrib/terminal/browser/media/terminal.css index 91c80d201f342..6283abc9020a6 100644 --- a/src/vs/workbench/contrib/terminal/browser/media/terminal.css +++ b/src/vs/workbench/contrib/terminal/browser/media/terminal.css @@ -522,32 +522,40 @@ background-color: var(--vscode-terminal-hoverHighlightBackground); } +.xterm-detached-instance .force-scrollbar .xterm .xterm-viewport, .monaco-workbench .editor-instance .force-scrollbar .xterm .xterm-viewport, .monaco-workbench .pane-body.integrated-terminal .force-scrollbar .xterm .xterm-viewport, +.xterm-detached-instance .xterm.focus .xterm-viewport, .monaco-workbench .editor-instance .xterm.focus .xterm-viewport, .monaco-workbench .pane-body.integrated-terminal .xterm.focus .xterm-viewport, +.xterm-detached-instance .xterm:focus .xterm-viewport, .monaco-workbench .editor-instance .xterm:focus .xterm-viewport, .monaco-workbench .pane-body.integrated-terminal .xterm:focus .xterm-viewport, +.xterm-detached-instance .xterm:hover .xterm-viewport, .monaco-workbench .editor-instance .xterm:hover .xterm-viewport, .monaco-workbench .pane-body.integrated-terminal .xterm:hover .xterm-viewport { background-color: var(--vscode-scrollbarSlider-background) !important; } +.xterm-detached-instance .xterm-viewport, .monaco-workbench .editor-instance .xterm-viewport, .monaco-workbench .pane-body.integrated-terminal .xterm-viewport { scrollbar-color: var(--vscode-scrollbarSlider-background) transparent; } +.xterm-detached-instance .xterm .xterm-viewport::-webkit-scrollbar-thumb:hover, .monaco-workbench .editor-instance .xterm .xterm-viewport::-webkit-scrollbar-thumb:hover, .monaco-workbench .pane-body.integrated-terminal .xterm .xterm-viewport::-webkit-scrollbar-thumb:hover { background-color: var(--vscode-scrollbarSlider-hoverBackground); } +.xterm-detached-instance .xterm-viewport:hover, .monaco-workbench .editor-instance .xterm-viewport:hover, .monaco-workbench .pane-body.integrated-terminal .xterm-viewport:hover { scrollbar-color: var(--vscode-scrollbarSlider-hoverBackground) transparent; } +.xterm-detached-instance .xterm .xterm-viewport::-webkit-scrollbar-thumb:active, .monaco-workbench .editor-instance .xterm .xterm-viewport::-webkit-scrollbar-thumb:active, .monaco-workbench .pane-body.integrated-terminal .xterm .xterm-viewport::-webkit-scrollbar-thumb:active { background-color: var(--vscode-scrollbarSlider-activeBackground); diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.ts b/src/vs/workbench/contrib/terminal/browser/terminal.ts index bf92606c93e01..60429c0775676 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.ts @@ -138,6 +138,11 @@ export const enum TerminalConnectionState { Connected } +export interface IDetachedXTermOptions { + cols: number; + rows: number; +} + export interface ITerminalService extends ITerminalInstanceHost { readonly _serviceBrand: undefined; @@ -170,6 +175,13 @@ export interface ITerminalService extends ITerminalInstanceHost { */ createTerminal(options?: ICreateTerminalOptions): Promise; + /** + * Creates a detached xterm instance which is not attached to the DOM or + * tracked as a terminal instance. + * @params options The options to create the terminal with + */ + createDetachedXterm(options: IDetachedXTermOptions): Promise; + /** * Creates a raw terminal instance, this should not be used outside of the terminal part. */ @@ -946,7 +958,19 @@ export const enum XtermTerminalConstants { SearchHighlightLimit = 1000 } -export interface IXtermTerminal { +export const enum RendererType { + WebGL = 1 << 0, + Canvas = 1 << 1, + Dom = 1 << 2, + All = RendererType.Canvas | RendererType.Dom | RendererType.Canvas, +} + +export interface IXtermTerminal extends IDisposable { + /** + * Underlying xterm terminal. + */ + readonly raw: RawXtermTerminal; + /** * An object that tracks when commands are run and enables navigating and selecting between * them. @@ -971,6 +995,13 @@ export interface IXtermTerminal { */ readonly isStdinDisabled: boolean; + /** + * Attached the terminal to the given element + * @param container Container the terminal will be rendered in + * @param enabledRenderers Bits of renderers that are allowable in this context. Defaults to all renderers if undefined. + */ + attachToElement(container: HTMLElement, enabledRenderers?: RendererType): void; + findResult?: { resultIndex: number; resultCount: number }; /** @@ -1021,6 +1052,11 @@ export interface IXtermTerminal { */ getBufferReverseIterator(): IterableIterator; + /** + * Gets the buffer contents as HTML. + */ + getContentsAsHtml(): Promise; + /** * Refreshes the terminal after it has been moved. */ diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index 6be78ea51649d..482b6d9a71dce 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -17,25 +17,28 @@ import { ErrorNoTelemetry, onUnexpectedError } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; import { KeyCode } from 'vs/base/common/keyCodes'; import { ISeparator, template } from 'vs/base/common/labels'; -import { Disposable, dispose, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, IDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; import * as path from 'vs/base/common/path'; -import { isMacintosh, isWindows, OperatingSystem, OS } from 'vs/base/common/platform'; +import { OS, OperatingSystem, isMacintosh, isWindows } from 'vs/base/common/platform'; import { ScrollbarVisibility } from 'vs/base/common/scrollable'; import { withNullAsUndefined } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import { TabFocus, TabFocusContext } from 'vs/editor/browser/config/tabFocus'; import * as nls from 'vs/nls'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; +import { AudioCue, IAudioCueService } from 'vs/platform/audioCues/browser/audioCueService'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { CodeDataTransfers, containsDragType } from 'vs/platform/dnd/browser/dnd'; +import { FileSystemProviderCapabilities, IFileService } from 'vs/platform/files/common/files'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { ResultKind } from 'vs/platform/keybinding/common/keybindingResolver'; import { ILogService } from 'vs/platform/log/common/log'; import { INotificationService, IPromptChoice, Severity } from 'vs/platform/notification/common/notification'; import { IOpenerService } from 'vs/platform/opener/common/opener'; @@ -45,18 +48,24 @@ import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storag import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IMarkProperties, ITerminalCommand, TerminalCapability } from 'vs/platform/terminal/common/capabilities/capabilities'; import { TerminalCapabilityStoreMultiplexer } from 'vs/platform/terminal/common/capabilities/terminalCapabilityStore'; +import { IEnvironmentVariableCollection, IMergedEnvironmentVariableCollection } from 'vs/platform/terminal/common/environmentVariable'; +import { deserializeEnvironmentVariableCollections } from 'vs/platform/terminal/common/environmentVariableShared'; import { IProcessDataEvent, IProcessPropertyMap, IReconnectionProperties, IShellLaunchConfig, ITerminalDimensionsOverride, ITerminalLaunchError, PosixShellType, ProcessPropertyType, ShellIntegrationStatus, TerminalExitReason, TerminalIcon, TerminalLocation, TerminalSettingId, TerminalShellType, TitleEventSource, WindowsShellType } from 'vs/platform/terminal/common/terminal'; import { formatMessageForTerminal } from 'vs/platform/terminal/common/terminalStrings'; +import { editorBackground } from 'vs/platform/theme/common/colorRegistry'; import { getIconRegistry } from 'vs/platform/theme/common/iconRegistry'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IWorkspaceContextService, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { IWorkspaceTrustRequestService } from 'vs/platform/workspace/common/workspaceTrust'; +import { PANEL_BACKGROUND, SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; import { IViewDescriptorService, IViewsService, ViewContainerLocation } from 'vs/workbench/common/views'; +import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityContribution'; import { TaskSettingId } from 'vs/workbench/contrib/tasks/common/tasks'; import { IRequestAddInstanceToGroupEvent, ITerminalContribution, ITerminalInstance, TerminalDataTransfers } from 'vs/workbench/contrib/terminal/browser/terminal'; import { TerminalLaunchHelpAction } from 'vs/workbench/contrib/terminal/browser/terminalActions'; import { TerminalConfigHelper } from 'vs/workbench/contrib/terminal/browser/terminalConfigHelper'; import { TerminalEditorInput } from 'vs/workbench/contrib/terminal/browser/terminalEditorInput'; +import { TerminalExtensionsRegistry } from 'vs/workbench/contrib/terminal/browser/terminalExtensions'; import { getColorClass, getColorStyleElement, getStandardColors } from 'vs/workbench/contrib/terminal/browser/terminalIcon'; import { TerminalProcessManager } from 'vs/workbench/contrib/terminal/browser/terminalProcessManager'; import { showRunRecentQuickPick } from 'vs/workbench/contrib/terminal/browser/terminalRunRecentQuickPick'; @@ -64,31 +73,21 @@ import { ITerminalStatusList, TerminalStatus, TerminalStatusList } from 'vs/work import { getTerminalResourcesFromDragEvent, getTerminalUri } from 'vs/workbench/contrib/terminal/browser/terminalUri'; import { TerminalWidgetManager } from 'vs/workbench/contrib/terminal/browser/widgets/widgetManager'; import { LineDataEventAddon } from 'vs/workbench/contrib/terminal/browser/xterm/lineDataEventAddon'; -import { XtermTerminal } from 'vs/workbench/contrib/terminal/browser/xterm/xtermTerminal'; +import { XtermTerminal, getXtermScaledDimensions } from 'vs/workbench/contrib/terminal/browser/xterm/xtermTerminal'; import { IEnvironmentVariableInfo } from 'vs/workbench/contrib/terminal/common/environmentVariable'; -import { deserializeEnvironmentVariableCollections } from 'vs/platform/terminal/common/environmentVariableShared'; import { getCommandHistory, getDirectoryHistory } from 'vs/workbench/contrib/terminal/common/history'; -import { DEFAULT_COMMANDS_TO_SKIP_SHELL, ITerminalProcessManager, ITerminalProfileResolverService, ProcessState, TerminalCommandId, TERMINAL_CREATION_COMMANDS, TERMINAL_VIEW_ID } from 'vs/workbench/contrib/terminal/common/terminal'; +import { DEFAULT_COMMANDS_TO_SKIP_SHELL, ITerminalProcessManager, ITerminalProfileResolverService, ProcessState, TERMINAL_CREATION_COMMANDS, TERMINAL_VIEW_ID, TerminalCommandId } from 'vs/workbench/contrib/terminal/common/terminal'; +import { TERMINAL_BACKGROUND_COLOR } from 'vs/workbench/contrib/terminal/common/terminalColorRegistry'; import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey'; +import { getWorkspaceForTerminal, preparePathForShell } from 'vs/workbench/contrib/terminal/common/terminalEnvironment'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IHistoryService } from 'vs/workbench/services/history/common/history'; import { IWorkbenchLayoutService, Position } from 'vs/workbench/services/layout/browser/layoutService'; import { IPathService } from 'vs/workbench/services/path/common/pathService'; import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; -import type { IMarker, Terminal as XTermTerminal } from 'xterm'; -import { IAudioCueService, AudioCue } from 'vs/platform/audioCues/browser/audioCueService'; -import { FileSystemProviderCapabilities, IFileService } from 'vs/platform/files/common/files'; -import { getWorkspaceForTerminal, preparePathForShell } from 'vs/workbench/contrib/terminal/common/terminalEnvironment'; -import { IEnvironmentVariableCollection, IMergedEnvironmentVariableCollection } from 'vs/platform/terminal/common/environmentVariable'; import { ISimpleSelectedSuggestion } from 'vs/workbench/services/suggest/browser/simpleSuggestWidget'; -import { TERMINAL_BACKGROUND_COLOR } from 'vs/workbench/contrib/terminal/common/terminalColorRegistry'; -import { editorBackground } from 'vs/platform/theme/common/colorRegistry'; -import { PANEL_BACKGROUND, SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; -import { TerminalExtensionsRegistry } from 'vs/workbench/contrib/terminal/browser/terminalExtensions'; -import { ResolvedKeybinding } from 'vs/base/common/keybindings'; -import { ResultKind } from 'vs/platform/keybinding/common/keybindingResolver'; -import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityContribution'; +import type { IMarker, Terminal as XTermTerminal } from 'xterm'; const enum Constants { /** @@ -105,19 +104,6 @@ const enum Constants { } let xtermConstructor: Promise | undefined; -function getXtermConstructor(keybinding?: ResolvedKeybinding): Promise { - if (xtermConstructor) { - return xtermConstructor; - } - xtermConstructor = Promises.withAsyncBody(async (resolve) => { - const Terminal = (await import('xterm')).Terminal; - // Localize strings - Terminal.strings.promptLabel = nls.localize('terminal.integrated.a11yPromptLabel', 'Terminal input'); - Terminal.strings.tooMuchOutput = keybinding ? nls.localize('terminal.integrated.useAccessibleBuffer', 'Use the accessible buffer {0} to manually review output', keybinding.getLabel()) : nls.localize('terminal.integrated.useAccessibleBufferNoKb', 'Use the Terminal: Focus Accessible Buffer command to manually review output'); - resolve(Terminal); - }); - return xtermConstructor; -} interface ICanvasDimensions { width: number; @@ -642,28 +628,15 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { } const font = this.xterm ? this.xterm.getFont() : this._configHelper.getFont(); - if (!font.charWidth || !font.charHeight) { + const newRC = getXtermScaledDimensions(font, dimension.width, dimension.height); + if (!newRC) { this._setLastKnownColsAndRows(); return null; } - // Because xterm.js converts from CSS pixels to actual pixels through - // the use of canvas, window.devicePixelRatio needs to be used here in - // order to be precise. font.charWidth/charHeight alone as insufficient - // when window.devicePixelRatio changes. - const scaledWidthAvailable = dimension.width * window.devicePixelRatio; - - const scaledCharWidth = font.charWidth * window.devicePixelRatio + font.letterSpacing; - const newCols = Math.max(Math.floor(scaledWidthAvailable / scaledCharWidth), 1); - - const scaledHeightAvailable = dimension.height * window.devicePixelRatio; - const scaledCharHeight = Math.ceil(font.charHeight * window.devicePixelRatio); - const scaledLineHeight = Math.floor(scaledCharHeight * font.lineHeight); - const newRows = Math.max(Math.floor(scaledHeightAvailable / scaledLineHeight), 1); - - if (this._cols !== newCols || this._rows !== newRows) { - this._cols = newCols; - this._rows = newRows; + if (this._cols !== newRC.cols || this._rows !== newRC.rows) { + this._cols = newRC.cols; + this._rows = newRC.rows; this._fireMaximumDimensionsChanged(); } @@ -704,11 +677,26 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { get persistentProcessId(): number | undefined { return this._processManager.persistentProcessId; } get shouldPersist(): boolean { return this._processManager.shouldPersist && !this.shellLaunchConfig.isTransient && (!this.reconnectionProperties || this._configurationService.getValue(TaskSettingId.Reconnection) === true); } + public static getXtermConstructor(keybindingService: IKeybindingService, contextKeyService: IContextKeyService) { + const keybinding = keybindingService.lookupKeybinding(TerminalCommandId.FocusAccessibleBuffer, contextKeyService); + if (xtermConstructor) { + return xtermConstructor; + } + xtermConstructor = Promises.withAsyncBody(async (resolve) => { + const Terminal = (await import('xterm')).Terminal; + // Localize strings + Terminal.strings.promptLabel = nls.localize('terminal.integrated.a11yPromptLabel', 'Terminal input'); + Terminal.strings.tooMuchOutput = keybinding ? nls.localize('terminal.integrated.useAccessibleBuffer', 'Use the accessible buffer {0} to manually review output', keybinding.getLabel()) : nls.localize('terminal.integrated.useAccessibleBufferNoKb', 'Use the Terminal: Focus Accessible Buffer command to manually review output'); + resolve(Terminal); + }); + return xtermConstructor; + } + /** * Create xterm.js instance and attach data listeners. */ protected async _createXterm(): Promise { - const Terminal = await getXtermConstructor(this._keybindingService.lookupKeybinding(TerminalCommandId.FocusAccessibleBuffer, this._contextKeyService)); + const Terminal = await TerminalInstance.getXtermConstructor(this._keybindingService, this._contextKeyService); if (this._isDisposed) { throw new ErrorNoTelemetry('Terminal disposed of during xterm.js creation'); } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalService.ts b/src/vs/workbench/contrib/terminal/browser/terminalService.ts index 4a2e36cceca24..8c620d8942d2a 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalService.ts @@ -30,7 +30,7 @@ import { ThemeIcon } from 'vs/base/common/themables'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { VirtualWorkspaceContext } from 'vs/workbench/common/contextkeys'; import { IEditableData, IViewsService } from 'vs/workbench/common/views'; -import { ICreateTerminalOptions, IRequestAddInstanceToGroupEvent, ITerminalEditorService, ITerminalGroup, ITerminalGroupService, ITerminalInstance, ITerminalInstanceHost, ITerminalInstanceService, ITerminalLocationOptions, ITerminalService, ITerminalServiceNativeDelegate, TerminalConnectionState, TerminalEditorLocation } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { ICreateTerminalOptions, IDetachedXTermOptions, IRequestAddInstanceToGroupEvent, ITerminalEditorService, ITerminalGroup, ITerminalGroupService, ITerminalInstance, ITerminalInstanceHost, ITerminalInstanceService, ITerminalLocationOptions, ITerminalService, ITerminalServiceNativeDelegate, IXtermTerminal, TerminalConnectionState, TerminalEditorLocation } from 'vs/workbench/contrib/terminal/browser/terminal'; import { getCwdForSplit } from 'vs/workbench/contrib/terminal/browser/terminalActions'; import { TerminalConfigHelper } from 'vs/workbench/contrib/terminal/browser/terminalConfigHelper'; import { TerminalEditorInput } from 'vs/workbench/contrib/terminal/browser/terminalEditorInput'; @@ -47,6 +47,11 @@ import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/ import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { ILifecycleService, ShutdownReason, StartupKind, WillShutdownEvent } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; +import { XtermTerminal } from 'vs/workbench/contrib/terminal/browser/xterm/xtermTerminal'; +import { TerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminalInstance'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { Color } from 'vs/base/common/color'; +import { emptyTerminalCapabilityStore } from 'vs/platform/terminal/common/capabilities/capabilities'; export class TerminalService implements ITerminalService { declare _serviceBrand: undefined; @@ -163,7 +168,8 @@ export class TerminalService implements ITerminalService { @IExtensionService private readonly _extensionService: IExtensionService, @INotificationService private readonly _notificationService: INotificationService, @IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService, - @ICommandService private readonly _commandService: ICommandService + @ICommandService private readonly _commandService: ICommandService, + @IKeybindingService private readonly _keybindingService: IKeybindingService ) { this._configHelper = this._instantiationService.createInstance(TerminalConfigHelper); // the below avoids having to poll routinely. @@ -971,6 +977,24 @@ export class TerminalService implements ITerminalService { return this._createTerminal(shellLaunchConfig, location, options); } + async createDetachedXterm(options: IDetachedXTermOptions): Promise { + const ctor = await TerminalInstance.getXtermConstructor(this._keybindingService, this._contextKeyService); + return this._instantiationService.createInstance( + XtermTerminal, + ctor, + this._configHelper, + options.cols, + options.rows, + { + getBackgroundColor: () => Color.transparent, + }, + emptyTerminalCapabilityStore, + '', + undefined, + false, + ); + } + private async _resolveCwd(shellLaunchConfig: IShellLaunchConfig, splitActiveTerminal: boolean, options?: ICreateTerminalOptions): Promise { const cwd = shellLaunchConfig.cwd; if (!cwd) { diff --git a/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts b/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts index 3995b6047737f..dcea92f31cbda 100644 --- a/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts @@ -18,7 +18,7 @@ import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; import { IShellIntegration, TerminalSettingId } from 'vs/platform/terminal/common/terminal'; import { ITerminalFont } from 'vs/workbench/contrib/terminal/common/terminal'; import { isSafari } from 'vs/base/browser/browser'; -import { IMarkTracker, IInternalXtermTerminal, IXtermTerminal, ISuggestController, IXtermColorProvider, XtermTerminalConstants } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { IMarkTracker, IInternalXtermTerminal, IXtermTerminal, ISuggestController, IXtermColorProvider, XtermTerminalConstants, RendererType } from 'vs/workbench/contrib/terminal/browser/terminal'; import { ILogService } from 'vs/platform/log/common/log'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { TerminalStorageKeys } from 'vs/workbench/contrib/terminal/common/terminalStorageKeys'; @@ -130,7 +130,7 @@ export class XtermTerminal extends DisposableStore implements IXtermTerminal, II private _shellIntegrationAddon: ShellIntegrationAddon; private _decorationAddon: DecorationAddon; - private _suggestAddon: SuggestAddon; + private _suggestAddon?: SuggestAddon; // Optional addons private _canvasAddon?: CanvasAddonType; @@ -181,7 +181,7 @@ export class XtermTerminal extends DisposableStore implements IXtermTerminal, II private readonly _backgroundColorProvider: IXtermColorProvider, private readonly _capabilities: ITerminalCapabilityStore, shellIntegrationNonce: string, - private readonly _terminalSuggestWidgetVisibleContextKey: IContextKey, + private readonly _terminalSuggestWidgetVisibleContextKey: IContextKey | undefined, disableShellIntegrationReporting: boolean, @IConfigurationService private readonly _configurationService: IConfigurationService, @IInstantiationService private readonly _instantiationService: IInstantiationService, @@ -257,12 +257,24 @@ export class XtermTerminal extends DisposableStore implements IXtermTerminal, II // Load the suggest addon, this should be loaded regardless of the setting as the sequences // may still come in - this._suggestAddon = this._instantiationService.createInstance(SuggestAddon, this._terminalSuggestWidgetVisibleContextKey); - this.raw.loadAddon(this._suggestAddon); - this._suggestAddon.onAcceptedCompletion(async text => { - this._onDidRequestFocus.fire(); - this._onDidRequestSendText.fire(text); - }); + if (this._terminalSuggestWidgetVisibleContextKey) { + this._suggestAddon = this._instantiationService.createInstance(SuggestAddon, this._terminalSuggestWidgetVisibleContextKey); + this.raw.loadAddon(this._suggestAddon); + this._suggestAddon.onAcceptedCompletion(async text => { + this._onDidRequestFocus.fire(); + this._onDidRequestSendText.fire(text); + }); + } + } + + async getContentsAsHtml(): Promise { + if (!this._serializeAddon) { + const Addon = await this._getSerializeAddonConstructor(); + this._serializeAddon = new Addon(); + this.raw.loadAddon(this._serializeAddon); + } + + return this._serializeAddon.serializeAsHTML(); } async getSelectionAsHtml(command?: ITerminalCommand): Promise { @@ -286,18 +298,18 @@ export class XtermTerminal extends DisposableStore implements IXtermTerminal, II return result; } - attachToElement(container: HTMLElement): HTMLElement { + attachToElement(container: HTMLElement, enabledRenderers: RendererType = RendererType.All): HTMLElement { if (!this._container) { this.raw.open(container); } // TODO: Move before open to the DOM renderer doesn't initialize - if (this._shouldLoadWebgl()) { + if ((enabledRenderers & RendererType.WebGL) && this._shouldLoadWebgl()) { this._enableWebglRenderer(); - } else if (this._shouldLoadCanvas()) { + } else if ((enabledRenderers & RendererType.Canvas) && this._shouldLoadCanvas()) { this._enableCanvasRenderer(); } - this._suggestAddon.setContainer(container); + this._suggestAddon?.setContainer(container); this._container = container; // Screen must be created at this point as xterm.open is called @@ -764,3 +776,25 @@ export class XtermTerminal extends DisposableStore implements IXtermTerminal, II this.raw.write(data); } } + +export function getXtermScaledDimensions(font: ITerminalFont, width: number, height: number) { + if (!font.charWidth || !font.charHeight) { + return null; + } + + // Because xterm.js converts from CSS pixels to actual pixels through + // the use of canvas, window.devicePixelRatio needs to be used here in + // order to be precise. font.charWidth/charHeight alone as insufficient + // when window.devicePixelRatio changes. + const scaledWidthAvailable = width * window.devicePixelRatio; + + const scaledCharWidth = font.charWidth * window.devicePixelRatio + font.letterSpacing; + const cols = Math.max(Math.floor(scaledWidthAvailable / scaledCharWidth), 1); + + const scaledHeightAvailable = height * window.devicePixelRatio; + const scaledCharHeight = Math.ceil(font.charHeight * window.devicePixelRatio); + const scaledLineHeight = Math.floor(scaledCharHeight * font.lineHeight); + const rows = Math.max(Math.floor(scaledHeightAvailable / scaledLineHeight), 1); + + return { rows, cols }; +} diff --git a/src/vs/workbench/contrib/testing/browser/media/testing.css b/src/vs/workbench/contrib/testing/browser/media/testing.css index e42b0ddbdfefb..11ae1f8784f42 100644 --- a/src/vs/workbench/contrib/testing/browser/media/testing.css +++ b/src/vs/workbench/contrib/testing/browser/media/testing.css @@ -186,8 +186,8 @@ border-bottom-width: 2px; } -.monaco-editor .zone-widget.test-output-peek .test-output-peek-message-container, -.monaco-editor .zone-widget.test-output-peek .test-output-peek-tree { +.test-output-peek-message-container, +.test-output-peek-tree { height: 100%; } diff --git a/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts b/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts index 3eec54c4a9719..4dbbaa20a27e9 100644 --- a/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts +++ b/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts @@ -34,7 +34,6 @@ import 'vs/css!./testingOutputPeek'; import { ICodeEditor, IDiffEditorConstructionOptions, isCodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditorAction2 } from 'vs/editor/browser/editorExtensions'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; -import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; import { DiffEditorWidget } from 'vs/editor/browser/widget/diffEditorWidget'; import { EmbeddedCodeEditorWidget, EmbeddedDiffEditorWidget } from 'vs/editor/browser/widget/embeddedCodeEditorWidget'; import { IDiffEditorOptions, IEditorOptions } from 'vs/editor/common/config/editorOptions'; @@ -66,6 +65,8 @@ import { IColorTheme, IThemeService } from 'vs/platform/theme/common/themeServic import { IViewPaneOptions, ViewPane } from 'vs/workbench/browser/parts/views/viewPane'; import { EditorModel } from 'vs/workbench/common/editor/editorModel'; import { IViewDescriptorService, IViewsService } from 'vs/workbench/common/views'; +import { ITerminalService, IXtermTerminal, RendererType } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { getXtermScaledDimensions } from 'vs/workbench/contrib/terminal/browser/xterm/xtermTerminal'; import { flatTestItemDelimiter } from 'vs/workbench/contrib/testing/browser/explorerProjections/display'; import { getTestItemContextOverlay } from 'vs/workbench/contrib/testing/browser/explorerProjections/testItemContextOverlay'; import * as icons from 'vs/workbench/contrib/testing/browser/icons'; @@ -723,7 +724,7 @@ class TestResultsViewContent extends Disposable { this.contentProviders = [ this._register(this.instantiationService.createInstance(DiffContentProvider, this.editor, messageContainer)), this._register(this.instantiationService.createInstance(MarkdownTestMessagePeek, messageContainer)), - this._register(this.instantiationService.createInstance(PlainTextMessagePeek, this.editor, messageContainer)), + this._register(this.instantiationService.createInstance(PlainTextMessagePeek, messageContainer)), ]; const treeContainer = dom.append(containerElement, dom.$('.test-output-peek-tree')); @@ -1139,65 +1140,122 @@ class MarkdownTestMessagePeek extends Disposable implements IPeekOutputRenderer } } + +const ttPolicy = window.trustedTypes?.createPolicy('outputTerminalData', { createHTML: value => value }); + class PlainTextMessagePeek extends Disposable implements IPeekOutputRenderer { - private readonly widget = this._register(new MutableDisposable()); - private readonly model = this._register(new MutableDisposable()); - private dimension?: dom.IDimension; + private static lastMeasurementInfo?: { font: string; metrics: TextMetrics }; + private static getTextMeasurements(xtermTerminal: IXtermTerminal) { + const opts = xtermTerminal.raw.options; + const font = `${opts.fontWeight || ''} ${opts.fontSize}px ${opts.fontFamily}`; + if (font === this.lastMeasurementInfo?.font) { + return this.lastMeasurementInfo.metrics; + } + + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d')!; + context.font = font; + // allow-any-unicode-next-line + const metrics = context.measureText('█'); + PlainTextMessagePeek.lastMeasurementInfo = { font, metrics }; + return metrics; + } + + private dimensions?: dom.IDimension; + + /** Active terminal instance. */ + private readonly terminal = this._register(new MutableDisposable()); + /** Used to display messages that get rendered to HTML statically */ + private readonly rawDisplayNode = this._register(new MutableDisposable()); + /** Listener for streaming result data */ + private readonly outputDataListener = this._register(new MutableDisposable()); constructor( - private readonly editor: ICodeEditor | undefined, private readonly container: HTMLElement, - @IInstantiationService private readonly instantiationService: IInstantiationService, - @ITextModelService private readonly modelService: ITextModelService, + @ITestResultService private readonly resultService: ITestResultService, + @ITerminalService private readonly terminalService: ITerminalService, ) { super(); } + private async makeTerminal() { + return this.terminal.value = await this.terminalService.createDetachedXterm({ rows: 10, cols: 80 }); + } + public async update(subject: InspectSubject) { - let uri: URI; if (subject instanceof MessageSubject) { const message = subject.messages[subject.messageIndex]; if (isDiffable(message) || typeof message.message !== 'string') { return this.clear(); } - uri = subject.messageUri; + const terminal = await this.makeTerminal(); + terminal.raw.write(message.message); + this.layoutTerminal(terminal); + this.renderTerminalToHtml(terminal); } else { - uri = subject.outputUri; - } + const result = this.resultService.getResult(subject.resultId)?.tasks[subject.taskIndex]; + if (!result) { + return this.clear(); + } + const terminal = await this.makeTerminal(); + for (const buffer of result.output.buffers) { + terminal.raw.write(buffer.buffer); + } - const modelRef = this.model.value = await this.modelService.createModelReference(uri); - if (!this.widget.value) { - this.widget.value = this.editor ? this.instantiationService.createInstance( - EmbeddedCodeEditorWidget, - this.container, - commonEditorOptions, - {}, - this.editor, - ) : this.instantiationService.createInstance( - CodeEditorWidget, - this.container, - commonEditorOptions, - { isSimpleWidget: true } - ); + terminal.raw.write('\x1b[?25l'); // hide cursor + this.layoutTerminal(terminal); - if (this.dimension) { - this.widget.value.layout(this.dimension); - } - } + this.rawDisplayNode.clear(); + this.container.classList.add('xterm-detached-instance'); + terminal.attachToElement(this.container, RendererType.Dom); - this.widget.value.setModel(modelRef.object.textEditorModel); - this.widget.value.updateOptions(commonEditorOptions); + this.outputDataListener.value = result.output.onDidWriteData(e => terminal.raw.write(e.buffer)); + } } private clear() { - this.model.clear(); - this.widget.clear(); + this.outputDataListener.clear(); + this.terminal.clear(); } public layout(dimensions: dom.IDimension) { - this.dimension = dimensions; - this.widget.value?.layout(dimensions); + this.dimensions = dimensions; + if (this.terminal.value) { + this.layoutTerminal(this.terminal.value, dimensions.width, dimensions.height); + } + } + + private async renderTerminalToHtml(xterm: IXtermTerminal) { + const html = await xterm.getContentsAsHtml(); + const wrapper = document.createElement('div'); + wrapper.innerHTML = (ttPolicy?.createHTML(html) ?? html) as string; + this.rawDisplayNode.value = toDisposable(() => this.container.removeChild(wrapper)); + this.container.appendChild(wrapper); + } + + private layoutTerminal( + xterm: IXtermTerminal, + width = this.dimensions?.width ?? this.container.clientWidth, + height = this.dimensions?.height ?? this.container.clientHeight + ) { + width -= 10; // scrollbar width + const scaled = getXtermScaledDimensions(xterm.getFont(), width, height); + + if (scaled) { + xterm.raw.resize(scaled.cols, scaled.rows); + } else { + const metrics = PlainTextMessagePeek.getTextMeasurements(xterm); + xterm.raw.resize( + Math.floor(width / metrics.width), + Math.floor(height / (metrics.fontBoundingBoxAscent + metrics.fontBoundingBoxDescent)), + ); + } + + // re-render the html with the width change: + if (this.rawDisplayNode.value) { + this.renderTerminalToHtml(xterm); + } } } From 59d8f2bd9c6ae3fdd0d3818c55abbe9251f72fb1 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Mon, 12 Jun 2023 12:11:22 -0700 Subject: [PATCH 02/10] wip --- .../terminal/browser/media/terminal.css | 2 +- .../contrib/terminal/browser/terminal.ts | 29 ++++++++------ .../terminal/browser/xterm/xtermTerminal.ts | 22 +++++++--- .../testing/browser/testingOutputPeek.ts | 40 ++++--------------- 4 files changed, 42 insertions(+), 51 deletions(-) diff --git a/src/vs/workbench/contrib/terminal/browser/media/terminal.css b/src/vs/workbench/contrib/terminal/browser/media/terminal.css index 6283abc9020a6..fdb309cc92218 100644 --- a/src/vs/workbench/contrib/terminal/browser/media/terminal.css +++ b/src/vs/workbench/contrib/terminal/browser/media/terminal.css @@ -245,7 +245,7 @@ top: 0; } -.monaco-workbench .pane-body.integrated-terminal .xterm .xterm-helper-textarea:focus { +.monaco-workbench .xterm .xterm-helper-textarea:focus { /* Override the general vscode style applies `opacity:1!important` to textareas */ opacity: 0 !important; } diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.ts b/src/vs/workbench/contrib/terminal/browser/terminal.ts index 60429c0775676..3ae187d55a199 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.ts @@ -958,19 +958,14 @@ export const enum XtermTerminalConstants { SearchHighlightLimit = 1000 } -export const enum RendererType { - WebGL = 1 << 0, - Canvas = 1 << 1, - Dom = 1 << 2, - All = RendererType.Canvas | RendererType.Dom | RendererType.Canvas, -} - -export interface IXtermTerminal extends IDisposable { +export interface IXtermAttachToElementOptions { /** - * Underlying xterm terminal. + * Whether GPU rendering should be enabled for this element, defaults to true. */ - readonly raw: RawXtermTerminal; + enableGpu?: boolean; +} +export interface IXtermTerminal extends IDisposable { /** * An object that tracks when commands are run and enables navigating and selecting between * them. @@ -998,9 +993,9 @@ export interface IXtermTerminal extends IDisposable { /** * Attached the terminal to the given element * @param container Container the terminal will be rendered in - * @param enabledRenderers Bits of renderers that are allowable in this context. Defaults to all renderers if undefined. + * @param options Additional options for mounting the terminal in an element */ - attachToElement(container: HTMLElement, enabledRenderers?: RendererType): void; + attachToElement(container: HTMLElement, options?: IXtermAttachToElementOptions): void; findResult?: { resultIndex: number; resultCount: number }; @@ -1019,6 +1014,16 @@ export interface IXtermTerminal extends IDisposable { */ forceRedraw(): void; + /** + * Writes data to the terminal. + */ + write(data: string | Uint8Array): void; + + /** + * Resizes the terminal. + */ + resize(columns: number, rows: number): void; + /** * Gets the font metrics of this xterm.js instance. */ diff --git a/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts b/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts index 5a2b30da03505..1d8e4b29d9227 100644 --- a/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts @@ -18,7 +18,7 @@ import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; import { IShellIntegration, TerminalSettingId } from 'vs/platform/terminal/common/terminal'; import { ITerminalFont } from 'vs/workbench/contrib/terminal/common/terminal'; import { isSafari } from 'vs/base/browser/browser'; -import { IMarkTracker, IInternalXtermTerminal, IXtermTerminal, ISuggestController, IXtermColorProvider, XtermTerminalConstants, RendererType } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { IMarkTracker, IInternalXtermTerminal, IXtermTerminal, ISuggestController, IXtermColorProvider, XtermTerminalConstants, IXtermAttachToElementOptions } from 'vs/workbench/contrib/terminal/browser/terminal'; import { ILogService } from 'vs/platform/log/common/log'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { TerminalStorageKeys } from 'vs/workbench/contrib/terminal/common/terminalStorageKeys'; @@ -298,15 +298,17 @@ export class XtermTerminal extends DisposableStore implements IXtermTerminal, II return result; } - attachToElement(container: HTMLElement, enabledRenderers: RendererType = RendererType.All): HTMLElement { + attachToElement(container: HTMLElement, { enableGpu = true }: IXtermAttachToElementOptions = {}): HTMLElement { if (!this._container) { this.raw.open(container); } // TODO: Move before open to the DOM renderer doesn't initialize - if ((enabledRenderers & RendererType.WebGL) && this._shouldLoadWebgl()) { - this._enableWebglRenderer(); - } else if ((enabledRenderers & RendererType.Canvas) && this._shouldLoadCanvas()) { - this._enableCanvasRenderer(); + if (enableGpu) { + if (this._shouldLoadWebgl()) { + this._enableWebglRenderer(); + } else if (this._shouldLoadCanvas()) { + this._enableCanvasRenderer(); + } } this._suggestAddon?.setContainer(container); @@ -316,6 +318,14 @@ export class XtermTerminal extends DisposableStore implements IXtermTerminal, II return this._container.querySelector('.xterm-screen')!; } + write(data: string | Uint8Array): void { + this.raw.write(data); + } + + resize(columns: number, rows: number): void { + this.raw.resize(columns, rows); + } + updateConfig(): void { const config = this._configHelper.config; this.raw.options.altClickMovesCursor = config.altClickMovesCursor; diff --git a/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts b/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts index 4dbbaa20a27e9..415a5792e2443 100644 --- a/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts +++ b/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts @@ -65,7 +65,7 @@ import { IColorTheme, IThemeService } from 'vs/platform/theme/common/themeServic import { IViewPaneOptions, ViewPane } from 'vs/workbench/browser/parts/views/viewPane'; import { EditorModel } from 'vs/workbench/common/editor/editorModel'; import { IViewDescriptorService, IViewsService } from 'vs/workbench/common/views'; -import { ITerminalService, IXtermTerminal, RendererType } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { ITerminalService, IXtermTerminal } from 'vs/workbench/contrib/terminal/browser/terminal'; import { getXtermScaledDimensions } from 'vs/workbench/contrib/terminal/browser/xterm/xtermTerminal'; import { flatTestItemDelimiter } from 'vs/workbench/contrib/testing/browser/explorerProjections/display'; import { getTestItemContextOverlay } from 'vs/workbench/contrib/testing/browser/explorerProjections/testItemContextOverlay'; @@ -1144,23 +1144,6 @@ class MarkdownTestMessagePeek extends Disposable implements IPeekOutputRenderer const ttPolicy = window.trustedTypes?.createPolicy('outputTerminalData', { createHTML: value => value }); class PlainTextMessagePeek extends Disposable implements IPeekOutputRenderer { - private static lastMeasurementInfo?: { font: string; metrics: TextMetrics }; - private static getTextMeasurements(xtermTerminal: IXtermTerminal) { - const opts = xtermTerminal.raw.options; - const font = `${opts.fontWeight || ''} ${opts.fontSize}px ${opts.fontFamily}`; - if (font === this.lastMeasurementInfo?.font) { - return this.lastMeasurementInfo.metrics; - } - - const canvas = document.createElement('canvas'); - const context = canvas.getContext('2d')!; - context.font = font; - // allow-any-unicode-next-line - const metrics = context.measureText('█'); - PlainTextMessagePeek.lastMeasurementInfo = { font, metrics }; - return metrics; - } - private dimensions?: dom.IDimension; /** Active terminal instance. */ @@ -1189,7 +1172,7 @@ class PlainTextMessagePeek extends Disposable implements IPeekOutputRenderer { return this.clear(); } const terminal = await this.makeTerminal(); - terminal.raw.write(message.message); + terminal.write(message.message); this.layoutTerminal(terminal); this.renderTerminalToHtml(terminal); } else { @@ -1200,17 +1183,17 @@ class PlainTextMessagePeek extends Disposable implements IPeekOutputRenderer { const terminal = await this.makeTerminal(); for (const buffer of result.output.buffers) { - terminal.raw.write(buffer.buffer); + terminal.write(buffer.buffer); } - terminal.raw.write('\x1b[?25l'); // hide cursor - this.layoutTerminal(terminal); + terminal.write('\x1b[?25l'); // hide cursor + requestAnimationFrame(() => this.layoutTerminal(terminal)); this.rawDisplayNode.clear(); this.container.classList.add('xterm-detached-instance'); - terminal.attachToElement(this.container, RendererType.Dom); + terminal.attachToElement(this.container, { enableGpu: false }); - this.outputDataListener.value = result.output.onDidWriteData(e => terminal.raw.write(e.buffer)); + this.outputDataListener.value = result.output.onDidWriteData(e => terminal.write(e.buffer)); } } @@ -1241,15 +1224,8 @@ class PlainTextMessagePeek extends Disposable implements IPeekOutputRenderer { ) { width -= 10; // scrollbar width const scaled = getXtermScaledDimensions(xterm.getFont(), width, height); - if (scaled) { - xterm.raw.resize(scaled.cols, scaled.rows); - } else { - const metrics = PlainTextMessagePeek.getTextMeasurements(xterm); - xterm.raw.resize( - Math.floor(width / metrics.width), - Math.floor(height / (metrics.fontBoundingBoxAscent + metrics.fontBoundingBoxDescent)), - ); + xterm.resize(scaled.cols, scaled.rows); } // re-render the html with the width change: From 1ccb2eb97e505fdcc1a4ed8ff1debd2c2d399903 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Mon, 12 Jun 2023 16:49:17 -0700 Subject: [PATCH 03/10] it works --- .../workbench/workbench-dev.html | 1 - .../electron-sandbox/workbench/workbench.html | 1 - .../terminal/browser/media/scrollbar.css | 44 ++---- .../terminal/browser/media/terminal.css | 32 ++--- .../contrib/terminal/browser/terminal.ts | 22 ++- .../terminal/browser/terminalActions.ts | 126 +++++++++++------- .../terminal/browser/terminalInstance.ts | 26 +--- .../terminal/browser/terminalService.ts | 11 +- .../terminal/browser/xterm/xtermTerminal.ts | 97 +++++++++++++- .../terminal/common/terminalContextKey.ts | 8 ++ .../testing/browser/testingOutputPeek.ts | 95 ++++++++----- 11 files changed, 294 insertions(+), 169 deletions(-) diff --git a/src/vs/code/electron-sandbox/workbench/workbench-dev.html b/src/vs/code/electron-sandbox/workbench/workbench-dev.html index 0ca432c742e29..94adff770121d 100644 --- a/src/vs/code/electron-sandbox/workbench/workbench-dev.html +++ b/src/vs/code/electron-sandbox/workbench/workbench-dev.html @@ -58,7 +58,6 @@ notebookRenderer stickyScrollViewLayer tokenizeToString - outputTerminalData ; "/> diff --git a/src/vs/code/electron-sandbox/workbench/workbench.html b/src/vs/code/electron-sandbox/workbench/workbench.html index 2db7189f7c26a..369eaac1f252e 100644 --- a/src/vs/code/electron-sandbox/workbench/workbench.html +++ b/src/vs/code/electron-sandbox/workbench/workbench.html @@ -58,7 +58,6 @@ notebookRenderer stickyScrollViewLayer tokenizeToString - outputTerminalData ; "/> diff --git a/src/vs/workbench/contrib/terminal/browser/media/scrollbar.css b/src/vs/workbench/contrib/terminal/browser/media/scrollbar.css index b267692727c66..4f3a711061b89 100644 --- a/src/vs/workbench/contrib/terminal/browser/media/scrollbar.css +++ b/src/vs/workbench/contrib/terminal/browser/media/scrollbar.css @@ -3,9 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -.xterm-detached-instance .xterm-viewport, -.monaco-workbench .editor-instance .xterm-viewport, -.monaco-workbench .pane-body.integrated-terminal .xterm-viewport { +.xterm-viewport { /* Use the hack presented in https://stackoverflow.com/a/38748186/1156119 to get opacity transitions working on the scrollbar */ -webkit-background-clip: text; background-clip: text; @@ -13,55 +11,35 @@ transition: background-color 800ms linear; } -.xterm-detached-instance .xterm-viewport, -.monaco-workbench .editor-instance .xterm-viewport, -.monaco-workbench .pane-body.integrated-terminal .xterm-viewport { +.xterm-viewport { scrollbar-width: thin; } -.xterm-detached-instance .xterm-viewport::-webkit-scrollbar, -.monaco-workbench .editor-instance .xterm-viewport::-webkit-scrollbar, -.monaco-workbench .pane-body.integrated-terminal .xterm-viewport::-webkit-scrollbar { +.xterm-viewport::-webkit-scrollbar { width: 10px; } -.xterm-detached-instance .xterm-viewport::-webkit-scrollbar-track, -.monaco-workbench .editor-instance .xterm-viewport::-webkit-scrollbar-track, -.monaco-workbench .pane-body.integrated-terminal .xterm-viewport::-webkit-scrollbar-track { +.xterm-viewport::-webkit-scrollbar-track { opacity: 0; } -.xterm-detached-instance .xterm-viewport::-webkit-scrollbar-thumb, -.monaco-workbench .editor-instance .xterm-viewport::-webkit-scrollbar-thumb, -.monaco-workbench .pane-body.integrated-terminal .xterm-viewport::-webkit-scrollbar-thumb { +.xterm-viewport::-webkit-scrollbar-thumb { min-height: 20px; background-color: inherit; } -.xterm-detached-instance .force-scrollbar .xterm .xterm-viewport, -.monaco-workbench .editor-instance .force-scrollbar .xterm .xterm-viewport, -.monaco-workbench .pane-body.integrated-terminal .force-scrollbar .xterm .xterm-viewport, -.xterm-detached-instance .xterm.focus .xterm-viewport, -.monaco-workbench .editor-instance .xterm.focus .xterm-viewport, -.monaco-workbench .pane-body.integrated-terminal .xterm.focus .xterm-viewport, -.xterm-detached-instance .xterm:focus .xterm-viewport, -.monaco-workbench .editor-instance .xterm:focus .xterm-viewport, -.monaco-workbench .pane-body.integrated-terminal .xterm:focus .xterm-viewport, -.xterm-detached-instance .xterm:hover .xterm-viewport, -.monaco-workbench .editor-instance .xterm:hover .xterm-viewport, -.monaco-workbench .pane-body.integrated-terminal .xterm:hover .xterm-viewport { +.force-scrollbar .xterm .xterm-viewport, +.xterm.focus .xterm-viewport, +.xterm:focus .xterm-viewport, +.xterm:hover .xterm-viewport { transition: opacity 100ms linear; cursor: default; } -.xterm-detached-instance .xterm .xterm-viewport::-webkit-scrollbar-thumb:hover, -.monaco-workbench .editor-instance .xterm .xterm-viewport::-webkit-scrollbar-thumb:hover, -.monaco-workbench .pane-body.integrated-terminal .xterm .xterm-viewport::-webkit-scrollbar-thumb:hover { +.xterm .xterm-viewport::-webkit-scrollbar-thumb:hover { transition: opacity 0ms linear; } -.xterm-detached-instance .xterm .xterm-viewport::-webkit-scrollbar-thumb:window-inactive, -.monaco-workbench .editor-instance .xterm .xterm-viewport::-webkit-scrollbar-thumb:window-inactive, -.monaco-workbench .pane-body.integrated-terminal .xterm .xterm-viewport::-webkit-scrollbar-thumb:window-inactive { +.xterm .xterm-viewport::-webkit-scrollbar-thumb:window-inactive { background-color: inherit; } diff --git a/src/vs/workbench/contrib/terminal/browser/media/terminal.css b/src/vs/workbench/contrib/terminal/browser/media/terminal.css index fdb309cc92218..684ebaa25778f 100644 --- a/src/vs/workbench/contrib/terminal/browser/media/terminal.css +++ b/src/vs/workbench/contrib/terminal/browser/media/terminal.css @@ -522,42 +522,26 @@ background-color: var(--vscode-terminal-hoverHighlightBackground); } -.xterm-detached-instance .force-scrollbar .xterm .xterm-viewport, -.monaco-workbench .editor-instance .force-scrollbar .xterm .xterm-viewport, -.monaco-workbench .pane-body.integrated-terminal .force-scrollbar .xterm .xterm-viewport, -.xterm-detached-instance .xterm.focus .xterm-viewport, -.monaco-workbench .editor-instance .xterm.focus .xterm-viewport, -.monaco-workbench .pane-body.integrated-terminal .xterm.focus .xterm-viewport, -.xterm-detached-instance .xterm:focus .xterm-viewport, -.monaco-workbench .editor-instance .xterm:focus .xterm-viewport, -.monaco-workbench .pane-body.integrated-terminal .xterm:focus .xterm-viewport, -.xterm-detached-instance .xterm:hover .xterm-viewport, -.monaco-workbench .editor-instance .xterm:hover .xterm-viewport, -.monaco-workbench .pane-body.integrated-terminal .xterm:hover .xterm-viewport { +.force-scrollbar .xterm .xterm-viewport, +.xterm.focus .xterm-viewport, +.xterm:focus .xterm-viewport, +.xterm:hover .xterm-viewport { background-color: var(--vscode-scrollbarSlider-background) !important; } -.xterm-detached-instance .xterm-viewport, -.monaco-workbench .editor-instance .xterm-viewport, -.monaco-workbench .pane-body.integrated-terminal .xterm-viewport { +.xterm-viewport { scrollbar-color: var(--vscode-scrollbarSlider-background) transparent; } -.xterm-detached-instance .xterm .xterm-viewport::-webkit-scrollbar-thumb:hover, -.monaco-workbench .editor-instance .xterm .xterm-viewport::-webkit-scrollbar-thumb:hover, -.monaco-workbench .pane-body.integrated-terminal .xterm .xterm-viewport::-webkit-scrollbar-thumb:hover { +.xterm .xterm-viewport::-webkit-scrollbar-thumb:hover { background-color: var(--vscode-scrollbarSlider-hoverBackground); } -.xterm-detached-instance .xterm-viewport:hover, -.monaco-workbench .editor-instance .xterm-viewport:hover, -.monaco-workbench .pane-body.integrated-terminal .xterm-viewport:hover { +.xterm-viewport:hover { scrollbar-color: var(--vscode-scrollbarSlider-hoverBackground) transparent; } -.xterm-detached-instance .xterm .xterm-viewport::-webkit-scrollbar-thumb:active, -.monaco-workbench .editor-instance .xterm .xterm-viewport::-webkit-scrollbar-thumb:active, -.monaco-workbench .pane-body.integrated-terminal .xterm .xterm-viewport::-webkit-scrollbar-thumb:active { +.xterm .xterm-viewport::-webkit-scrollbar-thumb:active { background-color: var(--vscode-scrollbarSlider-activeBackground); } diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.ts b/src/vs/workbench/contrib/terminal/browser/terminal.ts index 3ae187d55a199..9405017a22a82 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.ts @@ -148,6 +148,8 @@ export interface ITerminalService extends ITerminalInstanceHost { /** Gets all terminal instances, including editor and terminal view (group) instances. */ readonly instances: readonly ITerminalInstance[]; + /** Gets detached terminal instances created via {@link createDetachedXterm}. */ + readonly detachedInstances: Iterable; configHelper: ITerminalConfigHelper; isProcessSupportRegistered: boolean; readonly connectionState: TerminalConnectionState; @@ -719,11 +721,6 @@ export interface ITerminalInstance { */ resetFocusContextKey(): void; - /** - * Select all text in the terminal. - */ - selectAll(): void; - /** * Focuses the terminal instance if it's able to (the xterm.js instance must exist). * @@ -980,6 +977,11 @@ export interface IXtermTerminal extends IDisposable { readonly onDidChangeSelection: Event; readonly onDidChangeFindResults: Event<{ resultIndex: number; resultCount: number }>; + /** + * Event fired when focus enters (fires with true) or leaves (false) the terminal. + */ + readonly onDidChangeFocus: Event; + /** * Gets a view of the current texture atlas used by the renderers. */ @@ -990,6 +992,11 @@ export interface IXtermTerminal extends IDisposable { */ readonly isStdinDisabled: boolean; + /** + * Whether the terminal is currently focused. + */ + readonly isFocused: boolean; + /** * Attached the terminal to the given element * @param container Container the terminal will be rendered in @@ -1029,6 +1036,11 @@ export interface IXtermTerminal extends IDisposable { */ getFont(): ITerminalFont; + /** + * Prevents the terminal from handling keyboard events. + */ + setReadonly(): void; + /** Scroll the terminal buffer down 1 line. */ scrollDownLine(): void; /** Scroll the terminal buffer down 1 page. */ scrollDownPage(): void; /** Scroll the terminal buffer to the bottom. */ scrollToBottom(): void; diff --git a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts index 4623b8aad7fd2..7701e6db36be9 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts @@ -60,6 +60,8 @@ import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService import { TerminalCapability } from 'vs/platform/terminal/common/capabilities/capabilities'; import { killTerminalIcon, newTerminalIcon } from 'vs/workbench/contrib/terminal/browser/terminalIcons'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { XtermTerminal } from 'vs/workbench/contrib/terminal/browser/xterm/xtermTerminal'; +import { Iterable } from 'vs/base/common/iterator'; export const switchTerminalActionViewItemSeparator = '\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500'; export const switchTerminalShowTabsTitle = localize('showTerminalTabs', "Show Tabs"); @@ -179,6 +181,30 @@ export function registerActiveInstanceAction( }); } +/** + * A wrapper around {@link registerTerminalAction} that ensures an active instance exists and + * provides it to the run function. + */ +export function registerActiveTerminalAction( + options: IAction2Options & { run: (activeTerminal: XtermTerminal, accessor: ServicesAccessor, instance?: ITerminalInstance, args?: unknown) => void | Promise } +): IDisposable { + const originalRun = options.run; + return registerTerminalAction({ + ...options, + run: (c, accessor, args) => { + const activeDetached = Iterable.find(c.service.detachedInstances, d => d.isFocused); + if (activeDetached) { + return originalRun(activeDetached as XtermTerminal, accessor, undefined, args); + } + + const activeInstance = c.service.activeInstance; + if (activeInstance?.xterm) { + return originalRun(activeInstance.xterm, accessor, activeInstance, args); + } + } + }); +} + export interface ITerminalServicesCollection { service: ITerminalService; groupService: ITerminalGroupService; @@ -569,59 +595,59 @@ export function registerTerminalActions() { } }); - registerActiveInstanceAction({ + registerActiveTerminalAction({ id: TerminalCommandId.ScrollDownLine, title: { value: localize('workbench.action.terminal.scrollDown', "Scroll Down (Line)"), original: 'Scroll Down (Line)' }, keybinding: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.PageDown, linux: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.DownArrow }, - when: ContextKeyExpr.and(TerminalContextKeys.focus, TerminalContextKeys.altBufferActive.negate()), + when: ContextKeyExpr.and(TerminalContextKeys.focusInAny, TerminalContextKeys.altBufferActive.negate()), weight: KeybindingWeight.WorkbenchContrib }, precondition: ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), - run: (activeInstance) => activeInstance.scrollDownLine() + run: (xterm) => xterm.scrollDownLine() }); - registerActiveInstanceAction({ + registerActiveTerminalAction({ id: TerminalCommandId.ScrollDownPage, title: { value: localize('workbench.action.terminal.scrollDownPage', "Scroll Down (Page)"), original: 'Scroll Down (Page)' }, keybinding: { primary: KeyMod.Shift | KeyCode.PageDown, mac: { primary: KeyCode.PageDown }, - when: ContextKeyExpr.and(TerminalContextKeys.focus, TerminalContextKeys.altBufferActive.negate()), + when: ContextKeyExpr.and(TerminalContextKeys.focusInAny, TerminalContextKeys.altBufferActive.negate()), weight: KeybindingWeight.WorkbenchContrib }, precondition: ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), - run: (activeInstance) => activeInstance.scrollDownPage() + run: (xterm) => xterm.scrollDownPage() }); - registerActiveInstanceAction({ + registerActiveTerminalAction({ id: TerminalCommandId.ScrollToBottom, title: { value: localize('workbench.action.terminal.scrollToBottom', "Scroll to Bottom"), original: 'Scroll to Bottom' }, keybinding: { primary: KeyMod.CtrlCmd | KeyCode.End, linux: { primary: KeyMod.Shift | KeyCode.End }, - when: ContextKeyExpr.and(TerminalContextKeys.focus, TerminalContextKeys.altBufferActive.negate()), + when: ContextKeyExpr.and(TerminalContextKeys.focusInAny, TerminalContextKeys.altBufferActive.negate()), weight: KeybindingWeight.WorkbenchContrib }, precondition: ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), - run: (activeInstance) => activeInstance.scrollToBottom() + run: (xterm) => xterm.scrollToBottom() }); - registerActiveInstanceAction({ + registerActiveTerminalAction({ id: TerminalCommandId.ScrollUpLine, title: { value: localize('workbench.action.terminal.scrollUp', "Scroll Up (Line)"), original: 'Scroll Up (Line)' }, keybinding: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.PageUp, linux: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.UpArrow }, - when: ContextKeyExpr.and(TerminalContextKeys.focus, TerminalContextKeys.altBufferActive.negate()), + when: ContextKeyExpr.and(TerminalContextKeys.focusInAny, TerminalContextKeys.altBufferActive.negate()), weight: KeybindingWeight.WorkbenchContrib }, precondition: ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), - run: (activeInstance) => activeInstance.scrollUpLine() + run: (xterm) => xterm.scrollUpLine() }); - registerActiveInstanceAction({ + registerActiveTerminalAction({ id: TerminalCommandId.ScrollUpPage, title: { value: localize('workbench.action.terminal.scrollUpPage', "Scroll Up (Page)"), original: 'Scroll Up (Page)' }, f1: true, @@ -629,38 +655,38 @@ export function registerTerminalActions() { keybinding: { primary: KeyMod.Shift | KeyCode.PageUp, mac: { primary: KeyCode.PageUp }, - when: ContextKeyExpr.and(TerminalContextKeys.focus, TerminalContextKeys.altBufferActive.negate()), + when: ContextKeyExpr.and(TerminalContextKeys.focusInAny, TerminalContextKeys.altBufferActive.negate()), weight: KeybindingWeight.WorkbenchContrib }, precondition: ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), - run: (activeInstance) => activeInstance.scrollUpPage() + run: (xterm) => xterm.scrollUpPage() }); - registerActiveInstanceAction({ + registerActiveTerminalAction({ id: TerminalCommandId.ScrollToTop, title: { value: localize('workbench.action.terminal.scrollToTop', "Scroll to Top"), original: 'Scroll to Top' }, keybinding: { primary: KeyMod.CtrlCmd | KeyCode.Home, linux: { primary: KeyMod.Shift | KeyCode.Home }, - when: ContextKeyExpr.and(TerminalContextKeys.focus, TerminalContextKeys.altBufferActive.negate()), + when: ContextKeyExpr.and(TerminalContextKeys.focusInAny, TerminalContextKeys.altBufferActive.negate()), weight: KeybindingWeight.WorkbenchContrib }, precondition: ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), - run: (activeInstance) => activeInstance.scrollToTop() + run: (xterm) => xterm.scrollToTop() }); - registerActiveInstanceAction({ + registerActiveTerminalAction({ id: TerminalCommandId.ClearSelection, title: { value: localize('workbench.action.terminal.clearSelection', "Clear Selection"), original: 'Clear Selection' }, keybinding: { primary: KeyCode.Escape, - when: ContextKeyExpr.and(TerminalContextKeys.focus, TerminalContextKeys.textSelected, TerminalContextKeys.notFindVisible), + when: ContextKeyExpr.and(TerminalContextKeys.focusInAny, TerminalContextKeys.textSelected, TerminalContextKeys.notFindVisible), weight: KeybindingWeight.WorkbenchContrib }, precondition: ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), - run: (activeInstance) => { - if (activeInstance.hasSelection()) { - activeInstance.clearSelection(); + run: (xterm) => { + if (xterm.hasSelection()) { + xterm.clearSelection(); } } }); @@ -879,23 +905,25 @@ export function registerTerminalActions() { } }); - registerActiveInstanceAction({ + registerActiveTerminalAction({ id: TerminalCommandId.SelectToPreviousLine, title: { value: localize('workbench.action.terminal.selectToPreviousLine', "Select To Previous Line"), original: 'Select To Previous Line' }, precondition: ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), - run: async (activeInstance) => { - activeInstance.xterm?.markTracker.selectToPreviousLine(); - activeInstance.focus(); + run: async (xterm, _, instance) => { + xterm.markTracker.selectToPreviousLine(); + // prefer to call focus on the TerminalInstance for additional accessibility triggers + (instance || xterm).focus(); } }); - registerActiveInstanceAction({ + registerActiveTerminalAction({ id: TerminalCommandId.SelectToNextLine, title: { value: localize('workbench.action.terminal.selectToNextLine', "Select To Next Line"), original: 'Select To Next Line' }, precondition: ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), - run: async (activeInstance) => { - activeInstance.xterm?.markTracker.selectToNextLine(); - activeInstance.focus(); + run: async (xterm, _, instance) => { + xterm.markTracker.selectToNextLine(); + // prefer to call focus on the TerminalInstance for additional accessibility triggers + (instance || xterm).focus(); } }); @@ -1152,7 +1180,7 @@ export function registerTerminalActions() { } }); - registerActiveInstanceAction({ + registerActiveTerminalAction({ id: TerminalCommandId.SelectAll, title: { value: localize('workbench.action.terminal.selectAll', "Select All"), original: 'Select All' }, precondition: ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), @@ -1165,9 +1193,9 @@ export function registerTerminalActions() { // makes it easier for users to see how it works though. mac: { primary: KeyMod.CtrlCmd | KeyCode.KeyA }, weight: KeybindingWeight.WorkbenchContrib, - when: TerminalContextKeys.focus + when: TerminalContextKeys.focusInAny }], - run: (activeInstance) => activeInstance.selectAll() + run: (xterm) => xterm.selectAll() }); registerTerminalAction({ @@ -1455,42 +1483,48 @@ export function registerTerminalActions() { // Some commands depend on platform features if (BrowserFeatures.clipboard.writeText) { - registerActiveInstanceAction({ + registerActiveTerminalAction({ id: TerminalCommandId.CopySelection, title: { value: localize('workbench.action.terminal.copySelection', "Copy Selection"), original: 'Copy Selection' }, // TODO: Why is copy still showing up when text isn't selected? - precondition: ContextKeyExpr.and(ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), TerminalContextKeys.textSelected), + precondition: ContextKeyExpr.or(TerminalContextKeys.textSelectedInFocused, ContextKeyExpr.and(ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), TerminalContextKeys.textSelected)), keybinding: [{ primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyC, mac: { primary: KeyMod.CtrlCmd | KeyCode.KeyC }, weight: KeybindingWeight.WorkbenchContrib, - when: ContextKeyExpr.and(TerminalContextKeys.textSelected, TerminalContextKeys.focus) + when: ContextKeyExpr.or( + ContextKeyExpr.and(TerminalContextKeys.textSelected, TerminalContextKeys.focus), + TerminalContextKeys.textSelectedInFocused, + ) }], run: (activeInstance) => activeInstance.copySelection() }); - registerActiveInstanceAction({ + registerActiveTerminalAction({ id: TerminalCommandId.CopyAndClearSelection, title: { value: localize('workbench.action.terminal.copyAndClearSelection', "Copy and Clear Selection"), original: 'Copy and Clear Selection' }, - precondition: ContextKeyExpr.and(ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), TerminalContextKeys.textSelected), + precondition: ContextKeyExpr.or(TerminalContextKeys.textSelectedInFocused, ContextKeyExpr.and(ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), TerminalContextKeys.textSelected)), keybinding: [{ win: { primary: KeyMod.CtrlCmd | KeyCode.KeyC }, weight: KeybindingWeight.WorkbenchContrib, - when: ContextKeyExpr.and(TerminalContextKeys.textSelected, TerminalContextKeys.focus) + when: ContextKeyExpr.or( + ContextKeyExpr.and(TerminalContextKeys.textSelected, TerminalContextKeys.focus), + TerminalContextKeys.textSelectedInFocused, + ) }], - run: async (activeInstance) => { - await activeInstance.copySelection(); - activeInstance.clearSelection(); + run: async (xterm) => { + await xterm.copySelection(); + xterm.clearSelection(); } }); - registerActiveInstanceAction({ + registerActiveTerminalAction({ id: TerminalCommandId.CopySelectionAsHtml, title: { value: localize('workbench.action.terminal.copySelectionAsHtml', "Copy Selection as HTML"), original: 'Copy Selection as HTML' }, f1: true, category, - precondition: ContextKeyExpr.and(ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), TerminalContextKeys.textSelected), - run: (activeInstance) => activeInstance.copySelection(true) + precondition: ContextKeyExpr.or(TerminalContextKeys.textSelectedInFocused, ContextKeyExpr.and(ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), TerminalContextKeys.textSelected)), + run: (xterm) => xterm.copySelection(true) }); } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index 047817e9dde8b..7a48be17e03bc 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -1060,25 +1060,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { async copySelection(asHtml?: boolean, command?: ITerminalCommand): Promise { const xterm = await this._xtermReadyPromise; - if (this.hasSelection() || (asHtml && command)) { - if (asHtml) { - const textAsHtml = await xterm.getSelectionAsHtml(command); - function listener(e: any) { - if (!e.clipboardData.types.includes('text/plain')) { - e.clipboardData.setData('text/plain', command?.getOutput() ?? ''); - } - e.clipboardData.setData('text/html', textAsHtml); - e.preventDefault(); - } - document.addEventListener('copy', listener); - document.execCommand('copy'); - document.removeEventListener('copy', listener); - } else { - await this._clipboardService.writeText(xterm.raw.getSelection()); - } - } else { - this._notificationService.warn(nls.localize('terminal.integrated.copySelection.noSelection', 'The terminal has no selection to copy')); - } + await xterm.copySelection(asHtml, command); } get selection(): string | undefined { @@ -1089,12 +1071,6 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { this.xterm?.raw.clearSelection(); } - selectAll(): void { - // Focus here to ensure the terminal context key is set - this.xterm?.raw.focus(); - this.xterm?.raw.selectAll(); - } - private _refreshAltBufferContextKey() { this._terminalAltBufferActiveContextKey.set(!!(this.xterm && this.xterm.raw.buffer.active === this.xterm.raw.buffer.alternate)); } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalService.ts b/src/vs/workbench/contrib/terminal/browser/terminalService.ts index 8c620d8942d2a..aa3bf6a160050 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalService.ts @@ -58,6 +58,7 @@ export class TerminalService implements ITerminalService { private _hostActiveTerminals: Map = new Map(); + private _detachedInstances = new Set(); private _terminalEditorActive: IContextKey; private readonly _terminalShellTypeContextKey: IContextKey; @@ -85,6 +86,9 @@ export class TerminalService implements ITerminalService { get instances(): ITerminalInstance[] { return this._terminalGroupService.instances.concat(this._terminalEditorService.instances); } + get detachedInstances(): Iterable { + return this._detachedInstances; + } private _reconnectedTerminals: Map = new Map(); getReconnectedTerminals(reconnectionOwner: string): ITerminalInstance[] | undefined { @@ -979,7 +983,7 @@ export class TerminalService implements ITerminalService { async createDetachedXterm(options: IDetachedXTermOptions): Promise { const ctor = await TerminalInstance.getXtermConstructor(this._keybindingService, this._contextKeyService); - return this._instantiationService.createInstance( + const instance = this._instantiationService.createInstance( XtermTerminal, ctor, this._configHelper, @@ -993,6 +997,11 @@ export class TerminalService implements ITerminalService { undefined, false, ); + + this._detachedInstances.add(instance); + instance.onDidDispose(() => this._detachedInstances.delete(instance)); + + return instance; } private async _resolveCwd(shellLaunchConfig: IShellLaunchConfig, splitActiveTerminal: boolean, options?: ICreateTerminalOptions): Promise { diff --git a/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts b/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts index 1d8e4b29d9227..15de8892ea8ff 100644 --- a/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts @@ -10,6 +10,7 @@ import type { Unicode11Addon as Unicode11AddonType } from 'xterm-addon-unicode11 import type { WebglAddon as WebglAddonType } from 'xterm-addon-webgl'; import type { SerializeAddon as SerializeAddonType } from 'xterm-addon-serialize'; import type { ImageAddon as ImageAddonType } from 'xterm-addon-image'; +import * as dom from 'vs/base/browser/dom'; import { IXtermCore } from 'vs/workbench/contrib/terminal/browser/xterm-private'; import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { TerminalConfigHelper } from 'vs/workbench/contrib/terminal/browser/terminalConfigHelper'; @@ -35,7 +36,9 @@ import { ITerminalCapabilityStore, ITerminalCommand, TerminalCapability } from ' import { Emitter } from 'vs/base/common/event'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { SuggestAddon } from 'vs/workbench/contrib/terminal/browser/xterm/suggestAddon'; -import { IContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey'; +import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; const enum RenderConstants { /** @@ -139,6 +142,9 @@ export class XtermTerminal extends DisposableStore implements IXtermTerminal, II private _webglAddon?: WebglAddonType; private _serializeAddon?: SerializeAddonType; private _imageAddon?: ImageAddonType; + private readonly _attachedDisposables = this.add(new DisposableStore()); + private readonly _anyTerminalFocusContextKey: IContextKey; + private readonly _anyFocusedTerminalHasSelection: IContextKey; private _lastFindResult: { resultIndex: number; resultCount: number } | undefined; get findResult(): { resultIndex: number; resultCount: number } | undefined { return this._lastFindResult; } @@ -156,6 +162,10 @@ export class XtermTerminal extends DisposableStore implements IXtermTerminal, II readonly onDidChangeFindResults = this._onDidChangeFindResults.event; private readonly _onDidChangeSelection = new Emitter(); readonly onDidChangeSelection = this._onDidChangeSelection.event; + private readonly _onDidChangeFocus = new Emitter(); + readonly onDidChangeFocus = this._onDidChangeFocus.event; + private readonly _onDidDispose = new Emitter(); + readonly onDidDispose = this._onDidDispose.event; get markTracker(): IMarkTracker { return this._markNavigationAddon; } get shellIntegration(): IShellIntegration { return this._shellIntegrationAddon; } @@ -169,6 +179,8 @@ export class XtermTerminal extends DisposableStore implements IXtermTerminal, II return createImageBitmap(canvas); } + public isFocused = false; + /** * @param xtermCtor The xterm.js constructor, this is passed in so it can be fetched lazily * outside of this class such that {@link raw} is not nullable. @@ -189,7 +201,9 @@ export class XtermTerminal extends DisposableStore implements IXtermTerminal, II @INotificationService private readonly _notificationService: INotificationService, @IStorageService private readonly _storageService: IStorageService, @IThemeService private readonly _themeService: IThemeService, - @ITelemetryService private readonly _telemetryService: ITelemetryService + @ITelemetryService private readonly _telemetryService: ITelemetryService, + @IClipboardService private readonly _clipboardService: IClipboardService, + @IContextKeyService contextKeyService: IContextKeyService, ) { super(); const font = this._configHelper.getFont(undefined, true); @@ -242,7 +256,12 @@ export class XtermTerminal extends DisposableStore implements IXtermTerminal, II this.add(this._themeService.onDidColorThemeChange(theme => this._updateTheme(theme))); // Refire events - this.add(this.raw.onSelectionChange(() => this._onDidChangeSelection.fire())); + this.add(this.raw.onSelectionChange(() => { + this._onDidChangeSelection.fire(); + if (this.isFocused) { + this._anyFocusedTerminalHasSelection.set(this.raw.hasSelection()); + } + })); // Load addons this._updateUnicodeVersion(); @@ -255,6 +274,9 @@ export class XtermTerminal extends DisposableStore implements IXtermTerminal, II this._shellIntegrationAddon = this._instantiationService.createInstance(ShellIntegrationAddon, shellIntegrationNonce, disableShellIntegrationReporting, this._telemetryService); this.raw.loadAddon(this._shellIntegrationAddon); + this._anyTerminalFocusContextKey = TerminalContextKeys.focusInAny.bindTo(contextKeyService); + this._anyFocusedTerminalHasSelection = TerminalContextKeys.textSelectedInFocused.bindTo(contextKeyService); + // Load the suggest addon, this should be loaded regardless of the setting as the sequences // may still come in if (this._terminalSuggestWidgetVisibleContextKey) { @@ -302,6 +324,7 @@ export class XtermTerminal extends DisposableStore implements IXtermTerminal, II if (!this._container) { this.raw.open(container); } + // TODO: Move before open to the DOM renderer doesn't initialize if (enableGpu) { if (this._shouldLoadWebgl()) { @@ -311,6 +334,16 @@ export class XtermTerminal extends DisposableStore implements IXtermTerminal, II } } + if (!this.raw.element || !this.raw.textarea) { + throw new Error('xterm elements not set after open'); + } + + const ad = this._attachedDisposables; + ad.clear(); + ad.add(dom.addDisposableListener(this.raw.textarea, 'focus', () => this._setFocused(true))); + ad.add(dom.addDisposableListener(this.raw.textarea, 'blur', () => this._setFocused(false))); + ad.add(dom.addDisposableListener(this.raw.textarea, 'focusout', () => this._setFocused(false))); + this._suggestAddon?.setContainer(container); this._container = container; @@ -318,6 +351,15 @@ export class XtermTerminal extends DisposableStore implements IXtermTerminal, II return this._container.querySelector('.xterm-screen')!; } + private _setFocused(isFocused: boolean) { + if (isFocused !== this.isFocused) { + this.isFocused = isFocused; + this._onDidChangeFocus.fire(isFocused); + this._anyTerminalFocusContextKey.set(isFocused); + this._anyFocusedTerminalHasSelection.set(isFocused && this.raw.hasSelection()); + } + } + write(data: string | Uint8Array): void { this.raw.write(data); } @@ -516,6 +558,48 @@ export class XtermTerminal extends DisposableStore implements IXtermTerminal, II this._capabilities.get(TerminalCapability.CommandDetection)?.handleCommandStart(); } + hasSelection(): boolean { + return this.raw.hasSelection(); + } + + clearSelection(): void { + this.raw.clearSelection(); + } + + selectAll(): void { + this.raw.selectAll(); + } + + focus(): void { + this.raw.focus(); + } + + async copySelection(asHtml?: boolean, command?: ITerminalCommand): Promise { + if (this.hasSelection() || (asHtml && command)) { + if (asHtml) { + const textAsHtml = await this.getSelectionAsHtml(command); + function listener(e: any) { + if (!e.clipboardData.types.includes('text/plain')) { + e.clipboardData.setData('text/plain', command?.getOutput() ?? ''); + } + e.clipboardData.setData('text/html', textAsHtml); + e.preventDefault(); + } + document.addEventListener('copy', listener); + document.execCommand('copy'); + document.removeEventListener('copy', listener); + } else { + await this._clipboardService.writeText(this.raw.getSelection()); + } + } else { + this._notificationService.warn(localize('terminal.integrated.copySelection.noSelection', 'The terminal has no selection to copy')); + } + } + + setReadonly(): void { + this.raw.attachCustomKeyEventHandler(() => false); + } + private _setCursorBlink(blink: boolean): void { if (this.raw.options.cursorBlink !== blink) { this.raw.options.cursorBlink = blink; @@ -785,6 +869,13 @@ export class XtermTerminal extends DisposableStore implements IXtermTerminal, II _writeText(data: string): void { this.raw.write(data); } + + public override dispose(): void { + this._anyTerminalFocusContextKey.reset(); + this._anyFocusedTerminalHasSelection.reset(); + this._onDidDispose.fire(); + super.dispose(); + } } export function getXtermScaledDimensions(font: ITerminalFont, width: number, height: number) { diff --git a/src/vs/workbench/contrib/terminal/common/terminalContextKey.ts b/src/vs/workbench/contrib/terminal/common/terminalContextKey.ts index a3eb8693d018b..8e05935d10496 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalContextKey.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalContextKey.ts @@ -14,6 +14,7 @@ export const enum TerminalContextKeyStrings { HasFixedWidth = 'terminalHasFixedWidth', ProcessSupported = 'terminalProcessSupported', Focus = 'terminalFocus', + FocusInAny = 'terminalFocusInAny', AccessibleBufferFocus = 'terminalAccessibleBufferFocus', EditorFocus = 'terminalEditorFocus', TabsFocus = 'terminalTabsFocus', @@ -26,6 +27,7 @@ export const enum TerminalContextKeyStrings { A11yTreeFocus = 'terminalA11yTreeFocus', ViewShowing = 'terminalViewShowing', TextSelected = 'terminalTextSelected', + TextSelectedInFocused = 'terminalTextSelectedInFocused', FindVisible = 'terminalFindVisible', FindInputFocused = 'terminalFindInputFocused', FindFocused = 'terminalFindFocused', @@ -43,6 +45,9 @@ export namespace TerminalContextKeys { /** Whether the terminal is focused. */ export const focus = new RawContextKey(TerminalContextKeyStrings.Focus, false, localize('terminalFocusContextKey', "Whether the terminal is focused.")); + /** Whether any terminal is focused, including detached terminals used in other UI. */ + export const focusInAny = new RawContextKey(TerminalContextKeyStrings.FocusInAny, false, localize('terminalFocusInAnyContextKey', "Whether any terminal is focused, including detached terminals used in other UI.")); + /** Whether the accessible buffer is focused. */ export const accessibleBufferFocus = new RawContextKey(TerminalContextKeyStrings.AccessibleBufferFocus, false, localize('terminalAccessibleBufferFocusContextKey', "Whether the terminal accessible buffer is focused.")); @@ -94,6 +99,9 @@ export namespace TerminalContextKeys { /** Whether text is selected in the active terminal. */ export const textSelected = new RawContextKey(TerminalContextKeyStrings.TextSelected, false, localize('terminalTextSelectedContextKey', "Whether text is selected in the active terminal.")); + /** Whether text is selected in a focused terminal. Focused terminals are ones active in a terminal view or an editor, where a focused terminal simply has DOM focus. */ + export const textSelectedInFocused = new RawContextKey(TerminalContextKeyStrings.TextSelectedInFocused, false, localize('terminalTextSelectedInFocusedContextKey', "Whether text is selected in a focused terminal.")); + /** Whether text is NOT selected in the active terminal. */ export const notTextSelected = textSelected.toNegated(); diff --git a/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts b/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts index 415a5792e2443..764f8eaa7ab35 100644 --- a/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts +++ b/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts @@ -1141,15 +1141,11 @@ class MarkdownTestMessagePeek extends Disposable implements IPeekOutputRenderer } -const ttPolicy = window.trustedTypes?.createPolicy('outputTerminalData', { createHTML: value => value }); - class PlainTextMessagePeek extends Disposable implements IPeekOutputRenderer { private dimensions?: dom.IDimension; /** Active terminal instance. */ private readonly terminal = this._register(new MutableDisposable()); - /** Used to display messages that get rendered to HTML statically */ - private readonly rawDisplayNode = this._register(new MutableDisposable()); /** Listener for streaming result data */ private readonly outputDataListener = this._register(new MutableDisposable()); @@ -1162,10 +1158,21 @@ class PlainTextMessagePeek extends Disposable implements IPeekOutputRenderer { } private async makeTerminal() { - return this.terminal.value = await this.terminalService.createDetachedXterm({ rows: 10, cols: 80 }); + if (this.terminal.value) { + this.terminal.value.clearBuffer(); + this.terminal.value.clearSearchDecorations(); + return this.terminal.value; + } + + const terminal = this.terminal.value = await this.terminalService.createDetachedXterm({ rows: 10, cols: 80 }); + terminal.setReadonly(); + return terminal; } public async update(subject: InspectSubject) { + this.outputDataListener.clear(); + this.terminal.value?.clearBuffer(); + if (subject instanceof MessageSubject) { const message = subject.messages[subject.messageIndex]; if (isDiffable(message) || typeof message.message !== 'string') { @@ -1174,27 +1181,42 @@ class PlainTextMessagePeek extends Disposable implements IPeekOutputRenderer { const terminal = await this.makeTerminal(); terminal.write(message.message); this.layoutTerminal(terminal); - this.renderTerminalToHtml(terminal); + this.attachTerminalToDom(terminal); } else { - const result = this.resultService.getResult(subject.resultId)?.tasks[subject.taskIndex]; - if (!result) { + const result = this.resultService.getResult(subject.resultId); + const task = result?.tasks[subject.taskIndex]; + if (!task) { return this.clear(); } const terminal = await this.makeTerminal(); - for (const buffer of result.output.buffers) { - terminal.write(buffer.buffer); + if (result instanceof LiveTestResult) { + let hadData = false; + for (const buffer of task.output.buffers) { + hadData ||= buffer.byteLength > 0; + terminal.write(buffer.buffer); + } + if (!hadData && !task.running) { + this.writeNotice(terminal, localize('runNoOutout', 'The test run did not record any output.')); + } + } else { + this.writeNotice(terminal, localize('runNoOutputForPast', 'Test output is only available for new test runs.')); } - terminal.write('\x1b[?25l'); // hide cursor - requestAnimationFrame(() => this.layoutTerminal(terminal)); + this.attachTerminalToDom(terminal); + this.outputDataListener.value = task.output.onDidWriteData(e => terminal.write(e.buffer)); + } + } - this.rawDisplayNode.clear(); - this.container.classList.add('xterm-detached-instance'); - terminal.attachToElement(this.container, { enableGpu: false }); + private writeNotice(terminal: IXtermTerminal, str: string) { + terminal.write(`\x1b[1m${str}\x1b[0m`); + } - this.outputDataListener.value = result.output.onDidWriteData(e => terminal.write(e.buffer)); - } + private attachTerminalToDom(terminal: IXtermTerminal) { + terminal.write('\x1b[?25l'); // hide cursor + requestAnimationFrame(() => this.layoutTerminal(terminal)); + this.container.classList.add('xterm-detached-instance'); + terminal.attachToElement(this.container, { enableGpu: false }); } private clear() { @@ -1209,14 +1231,6 @@ class PlainTextMessagePeek extends Disposable implements IPeekOutputRenderer { } } - private async renderTerminalToHtml(xterm: IXtermTerminal) { - const html = await xterm.getContentsAsHtml(); - const wrapper = document.createElement('div'); - wrapper.innerHTML = (ttPolicy?.createHTML(html) ?? html) as string; - this.rawDisplayNode.value = toDisposable(() => this.container.removeChild(wrapper)); - this.container.appendChild(wrapper); - } - private layoutTerminal( xterm: IXtermTerminal, width = this.dimensions?.width ?? this.container.clientWidth, @@ -1227,11 +1241,6 @@ class PlainTextMessagePeek extends Disposable implements IPeekOutputRenderer { if (scaled) { xterm.resize(scaled.cols, scaled.rows); } - - // re-render the html with the width change: - if (this.rawDisplayNode.value) { - this.renderTerminalToHtml(xterm); - } } } @@ -1549,6 +1558,10 @@ class OutputPeekTree extends Disposable { const resultNode = cc.get(result)! as TestResultElement; const disposable = new DisposableStore(); disposable.add(result.onNewTask(() => { + if (result.tasks.length === 1) { + this.requestReveal.fire(new TaskSubject(result.id, 0)); // reveal the first task in new runs + } + if (this.tree.hasElement(resultNode)) { this.tree.setChildren(resultNode, getResultChildren(result), { diffIdentityProvider }); } @@ -2072,6 +2085,28 @@ export class ToggleTestingPeekHistory extends Action2 { } } +// export class CopyTestOutputSelection extends Action2 { +// public static readonly ID = 'testing.copyTestOutputSelection'; + +// constructor() { +// super({ +// id: ToggleTestingPeekHistory.ID, +// title: { value: localize('workbench.action.terminal.copySelection', "Copy Selection"), original: 'Copy Selection' }, +// keybinding: [{ +// primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyC, +// mac: { primary: KeyMod.CtrlCmd | KeyCode.KeyC }, +// weight: KeybindingWeight.WorkbenchContrib, +// when: ContextKeyExpr.and(TerminalContextKeys.textSelected, TerminalContextKeys.focus) +// }], +// }); +// } + +// public override run(accessor: ServicesAccessor) { +// const opener = accessor.get(ITestingPeekOpener); +// opener.historyVisible.value = !opener.historyVisible.value; +// } +// } + class CreationCache { private readonly v = new WeakMap(); From 714e6b6a99c90bf4d01e02006e0d38deb01b3056 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Tue, 13 Jun 2023 11:11:54 -0700 Subject: [PATCH 04/10] address pr comments --- .../terminal/browser/media/scrollbar.css | 22 +++---- .../contrib/terminal/browser/terminal.ts | 35 ++++++----- .../terminal/browser/terminalActions.ts | 34 +++++----- .../terminal/browser/terminalService.ts | 13 ++-- .../terminal/browser/xterm/xtermTerminal.ts | 51 ++++++++------- .../terminal/common/terminalContextKey.ts | 2 +- .../testing/browser/testingOutputPeek.ts | 63 +++++++++++++------ 7 files changed, 124 insertions(+), 96 deletions(-) diff --git a/src/vs/workbench/contrib/terminal/browser/media/scrollbar.css b/src/vs/workbench/contrib/terminal/browser/media/scrollbar.css index 4f3a711061b89..2a1f155fc8e68 100644 --- a/src/vs/workbench/contrib/terminal/browser/media/scrollbar.css +++ b/src/vs/workbench/contrib/terminal/browser/media/scrollbar.css @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -.xterm-viewport { +.monaco-workbench .xterm-viewport { /* Use the hack presented in https://stackoverflow.com/a/38748186/1156119 to get opacity transitions working on the scrollbar */ -webkit-background-clip: text; background-clip: text; @@ -11,35 +11,35 @@ transition: background-color 800ms linear; } -.xterm-viewport { +.monaco-workbench .xterm-viewport { scrollbar-width: thin; } -.xterm-viewport::-webkit-scrollbar { +.monaco-workbench .xterm-viewport::-webkit-scrollbar { width: 10px; } -.xterm-viewport::-webkit-scrollbar-track { +.monaco-workbench .xterm-viewport::-webkit-scrollbar-track { opacity: 0; } -.xterm-viewport::-webkit-scrollbar-thumb { +.monaco-workbench .xterm-viewport::-webkit-scrollbar-thumb { min-height: 20px; background-color: inherit; } -.force-scrollbar .xterm .xterm-viewport, -.xterm.focus .xterm-viewport, -.xterm:focus .xterm-viewport, -.xterm:hover .xterm-viewport { +.monaco-workbench .force-scrollbar .xterm .xterm-viewport, +.monaco-workbench .xterm.focus .xterm-viewport, +.monaco-workbench .xterm:focus .xterm-viewport, +.monaco-workbench .xterm:hover .xterm-viewport { transition: opacity 100ms linear; cursor: default; } -.xterm .xterm-viewport::-webkit-scrollbar-thumb:hover { +.monaco-workbench .xterm .xterm-viewport::-webkit-scrollbar-thumb:hover { transition: opacity 0ms linear; } -.xterm .xterm-viewport::-webkit-scrollbar-thumb:window-inactive { +.monaco-workbench .xterm .xterm-viewport::-webkit-scrollbar-thumb:window-inactive { background-color: inherit; } diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.ts b/src/vs/workbench/contrib/terminal/browser/terminal.ts index 9405017a22a82..2284cf96bb500 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.ts @@ -141,6 +141,8 @@ export const enum TerminalConnectionState { export interface IDetachedXTermOptions { cols: number; rows: number; + colorProvider: IXtermColorProvider; + readonly?: boolean; } export interface ITerminalService extends ITerminalInstanceHost { @@ -182,7 +184,7 @@ export interface ITerminalService extends ITerminalInstanceHost { * tracked as a terminal instance. * @params options The options to create the terminal with */ - createDetachedXterm(options: IDetachedXTermOptions): Promise; + createDetachedXterm(options: IDetachedXTermOptions): Promise; /** * Creates a raw terminal instance, this should not be used outside of the terminal part. @@ -959,7 +961,7 @@ export interface IXtermAttachToElementOptions { /** * Whether GPU rendering should be enabled for this element, defaults to true. */ - enableGpu?: boolean; + enableGpu: boolean; } export interface IXtermTerminal extends IDisposable { @@ -1002,7 +1004,7 @@ export interface IXtermTerminal extends IDisposable { * @param container Container the terminal will be rendered in * @param options Additional options for mounting the terminal in an element */ - attachToElement(container: HTMLElement, options?: IXtermAttachToElementOptions): void; + attachToElement(container: HTMLElement, options?: Partial): void; findResult?: { resultIndex: number; resultCount: number }; @@ -1021,25 +1023,11 @@ export interface IXtermTerminal extends IDisposable { */ forceRedraw(): void; - /** - * Writes data to the terminal. - */ - write(data: string | Uint8Array): void; - - /** - * Resizes the terminal. - */ - resize(columns: number, rows: number): void; - /** * Gets the font metrics of this xterm.js instance. */ getFont(): ITerminalFont; - /** - * Prevents the terminal from handling keyboard events. - */ - setReadonly(): void; /** Scroll the terminal buffer down 1 line. */ scrollDownLine(): void; /** Scroll the terminal buffer down 1 page. */ scrollDownPage(): void; @@ -1080,6 +1068,19 @@ export interface IXtermTerminal extends IDisposable { refresh(): void; } +export interface IDetachedXtermTerminal extends IXtermTerminal { + + /** + * Writes data to the terminal. + */ + write(data: string | Uint8Array): void; + + /** + * Resizes the terminal. + */ + resize(columns: number, rows: number): void; +} + export interface IInternalXtermTerminal { /** * Writes text directly to the terminal, bypassing the process. diff --git a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts index 7701e6db36be9..d74a174d111df 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts @@ -182,10 +182,12 @@ export function registerActiveInstanceAction( } /** - * A wrapper around {@link registerTerminalAction} that ensures an active instance exists and - * provides it to the run function. + * A wrapper around {@link registerTerminalAction} that ensures an active terminal + * exists and provides it to the run function. + * + * This includes detached xterm terminals that are not managed by an {@link ITerminalInstance}. */ -export function registerActiveTerminalAction( +export function registerActiveXtermAction( options: IAction2Options & { run: (activeTerminal: XtermTerminal, accessor: ServicesAccessor, instance?: ITerminalInstance, args?: unknown) => void | Promise } ): IDisposable { const originalRun = options.run; @@ -595,7 +597,7 @@ export function registerTerminalActions() { } }); - registerActiveTerminalAction({ + registerActiveXtermAction({ id: TerminalCommandId.ScrollDownLine, title: { value: localize('workbench.action.terminal.scrollDown', "Scroll Down (Line)"), original: 'Scroll Down (Line)' }, keybinding: { @@ -608,7 +610,7 @@ export function registerTerminalActions() { run: (xterm) => xterm.scrollDownLine() }); - registerActiveTerminalAction({ + registerActiveXtermAction({ id: TerminalCommandId.ScrollDownPage, title: { value: localize('workbench.action.terminal.scrollDownPage', "Scroll Down (Page)"), original: 'Scroll Down (Page)' }, keybinding: { @@ -621,7 +623,7 @@ export function registerTerminalActions() { run: (xterm) => xterm.scrollDownPage() }); - registerActiveTerminalAction({ + registerActiveXtermAction({ id: TerminalCommandId.ScrollToBottom, title: { value: localize('workbench.action.terminal.scrollToBottom', "Scroll to Bottom"), original: 'Scroll to Bottom' }, keybinding: { @@ -634,7 +636,7 @@ export function registerTerminalActions() { run: (xterm) => xterm.scrollToBottom() }); - registerActiveTerminalAction({ + registerActiveXtermAction({ id: TerminalCommandId.ScrollUpLine, title: { value: localize('workbench.action.terminal.scrollUp', "Scroll Up (Line)"), original: 'Scroll Up (Line)' }, keybinding: { @@ -647,7 +649,7 @@ export function registerTerminalActions() { run: (xterm) => xterm.scrollUpLine() }); - registerActiveTerminalAction({ + registerActiveXtermAction({ id: TerminalCommandId.ScrollUpPage, title: { value: localize('workbench.action.terminal.scrollUpPage', "Scroll Up (Page)"), original: 'Scroll Up (Page)' }, f1: true, @@ -662,7 +664,7 @@ export function registerTerminalActions() { run: (xterm) => xterm.scrollUpPage() }); - registerActiveTerminalAction({ + registerActiveXtermAction({ id: TerminalCommandId.ScrollToTop, title: { value: localize('workbench.action.terminal.scrollToTop', "Scroll to Top"), original: 'Scroll to Top' }, keybinding: { @@ -675,7 +677,7 @@ export function registerTerminalActions() { run: (xterm) => xterm.scrollToTop() }); - registerActiveTerminalAction({ + registerActiveXtermAction({ id: TerminalCommandId.ClearSelection, title: { value: localize('workbench.action.terminal.clearSelection', "Clear Selection"), original: 'Clear Selection' }, keybinding: { @@ -905,7 +907,7 @@ export function registerTerminalActions() { } }); - registerActiveTerminalAction({ + registerActiveXtermAction({ id: TerminalCommandId.SelectToPreviousLine, title: { value: localize('workbench.action.terminal.selectToPreviousLine', "Select To Previous Line"), original: 'Select To Previous Line' }, precondition: ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), @@ -916,7 +918,7 @@ export function registerTerminalActions() { } }); - registerActiveTerminalAction({ + registerActiveXtermAction({ id: TerminalCommandId.SelectToNextLine, title: { value: localize('workbench.action.terminal.selectToNextLine', "Select To Next Line"), original: 'Select To Next Line' }, precondition: ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), @@ -1180,7 +1182,7 @@ export function registerTerminalActions() { } }); - registerActiveTerminalAction({ + registerActiveXtermAction({ id: TerminalCommandId.SelectAll, title: { value: localize('workbench.action.terminal.selectAll', "Select All"), original: 'Select All' }, precondition: ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), @@ -1483,7 +1485,7 @@ export function registerTerminalActions() { // Some commands depend on platform features if (BrowserFeatures.clipboard.writeText) { - registerActiveTerminalAction({ + registerActiveXtermAction({ id: TerminalCommandId.CopySelection, title: { value: localize('workbench.action.terminal.copySelection', "Copy Selection"), original: 'Copy Selection' }, // TODO: Why is copy still showing up when text isn't selected? @@ -1500,7 +1502,7 @@ export function registerTerminalActions() { run: (activeInstance) => activeInstance.copySelection() }); - registerActiveTerminalAction({ + registerActiveXtermAction({ id: TerminalCommandId.CopyAndClearSelection, title: { value: localize('workbench.action.terminal.copyAndClearSelection', "Copy and Clear Selection"), original: 'Copy and Clear Selection' }, precondition: ContextKeyExpr.or(TerminalContextKeys.textSelectedInFocused, ContextKeyExpr.and(ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), TerminalContextKeys.textSelected)), @@ -1518,7 +1520,7 @@ export function registerTerminalActions() { } }); - registerActiveTerminalAction({ + registerActiveXtermAction({ id: TerminalCommandId.CopySelectionAsHtml, title: { value: localize('workbench.action.terminal.copySelectionAsHtml', "Copy Selection as HTML"), original: 'Copy Selection as HTML' }, f1: true, diff --git a/src/vs/workbench/contrib/terminal/browser/terminalService.ts b/src/vs/workbench/contrib/terminal/browser/terminalService.ts index aa3bf6a160050..1dec82d52c41f 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalService.ts @@ -30,7 +30,7 @@ import { ThemeIcon } from 'vs/base/common/themables'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { VirtualWorkspaceContext } from 'vs/workbench/common/contextkeys'; import { IEditableData, IViewsService } from 'vs/workbench/common/views'; -import { ICreateTerminalOptions, IDetachedXTermOptions, IRequestAddInstanceToGroupEvent, ITerminalEditorService, ITerminalGroup, ITerminalGroupService, ITerminalInstance, ITerminalInstanceHost, ITerminalInstanceService, ITerminalLocationOptions, ITerminalService, ITerminalServiceNativeDelegate, IXtermTerminal, TerminalConnectionState, TerminalEditorLocation } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { ICreateTerminalOptions, IDetachedXTermOptions, IDetachedXtermTerminal, IRequestAddInstanceToGroupEvent, ITerminalEditorService, ITerminalGroup, ITerminalGroupService, ITerminalInstance, ITerminalInstanceHost, ITerminalInstanceService, ITerminalLocationOptions, ITerminalService, ITerminalServiceNativeDelegate, IXtermTerminal, TerminalConnectionState, TerminalEditorLocation } from 'vs/workbench/contrib/terminal/browser/terminal'; import { getCwdForSplit } from 'vs/workbench/contrib/terminal/browser/terminalActions'; import { TerminalConfigHelper } from 'vs/workbench/contrib/terminal/browser/terminalConfigHelper'; import { TerminalEditorInput } from 'vs/workbench/contrib/terminal/browser/terminalEditorInput'; @@ -50,7 +50,6 @@ import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteA import { XtermTerminal } from 'vs/workbench/contrib/terminal/browser/xterm/xtermTerminal'; import { TerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminalInstance'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { Color } from 'vs/base/common/color'; import { emptyTerminalCapabilityStore } from 'vs/platform/terminal/common/capabilities/capabilities'; export class TerminalService implements ITerminalService { @@ -981,7 +980,7 @@ export class TerminalService implements ITerminalService { return this._createTerminal(shellLaunchConfig, location, options); } - async createDetachedXterm(options: IDetachedXTermOptions): Promise { + async createDetachedXterm(options: IDetachedXTermOptions): Promise { const ctor = await TerminalInstance.getXtermConstructor(this._keybindingService, this._contextKeyService); const instance = this._instantiationService.createInstance( XtermTerminal, @@ -989,15 +988,17 @@ export class TerminalService implements ITerminalService { this._configHelper, options.cols, options.rows, - { - getBackgroundColor: () => Color.transparent, - }, + options.colorProvider, emptyTerminalCapabilityStore, '', undefined, false, ); + if (options.readonly) { + instance.raw.attachCustomKeyEventHandler(() => false); + } + this._detachedInstances.add(instance); instance.onDidDispose(() => this._detachedInstances.delete(instance)); diff --git a/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts b/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts index 15de8892ea8ff..f22e41a6d718e 100644 --- a/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts @@ -19,7 +19,7 @@ import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; import { IShellIntegration, TerminalSettingId } from 'vs/platform/terminal/common/terminal'; import { ITerminalFont } from 'vs/workbench/contrib/terminal/common/terminal'; import { isSafari } from 'vs/base/browser/browser'; -import { IMarkTracker, IInternalXtermTerminal, IXtermTerminal, ISuggestController, IXtermColorProvider, XtermTerminalConstants, IXtermAttachToElementOptions } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { IMarkTracker, IInternalXtermTerminal, IXtermTerminal, ISuggestController, IXtermColorProvider, XtermTerminalConstants, IXtermAttachToElementOptions, IDetachedXtermTerminal } from 'vs/workbench/contrib/terminal/browser/terminal'; import { ILogService } from 'vs/platform/log/common/log'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { TerminalStorageKeys } from 'vs/workbench/contrib/terminal/common/terminalStorageKeys'; @@ -111,7 +111,7 @@ function getFullBufferLineAsString(lineIndex: number, buffer: IBuffer): { lineDa * Wraps the xterm object with additional functionality. Interaction with the backing process is out * of the scope of this class. */ -export class XtermTerminal extends DisposableStore implements IXtermTerminal, IInternalXtermTerminal { +export class XtermTerminal extends DisposableStore implements IXtermTerminal, IDetachedXtermTerminal, IInternalXtermTerminal { /** The raw xterm.js instance */ readonly raw: RawXtermTerminal; @@ -126,7 +126,7 @@ export class XtermTerminal extends DisposableStore implements IXtermTerminal, II } private _core: IXtermCore; private static _suggestedRendererType: 'canvas' | 'dom' | undefined = undefined; - private _container?: HTMLElement; + private _attached?: { container: HTMLElement; options: IXtermAttachToElementOptions }; // Always on addons private _markNavigationAddon: MarkNavigationAddon; @@ -179,7 +179,9 @@ export class XtermTerminal extends DisposableStore implements IXtermTerminal, II return createImageBitmap(canvas); } - public isFocused = false; + public get isFocused() { + return !!this.raw.element?.contains(document.activeElement); + } /** * @param xtermCtor The xterm.js constructor, this is passed in so it can be fetched lazily @@ -320,13 +322,14 @@ export class XtermTerminal extends DisposableStore implements IXtermTerminal, II return result; } - attachToElement(container: HTMLElement, { enableGpu = true }: IXtermAttachToElementOptions = {}): HTMLElement { - if (!this._container) { + attachToElement(container: HTMLElement, partialOptions?: Partial): HTMLElement { + const options: IXtermAttachToElementOptions = { enableGpu: true, ...partialOptions }; + if (!this._attached) { this.raw.open(container); } // TODO: Move before open to the DOM renderer doesn't initialize - if (enableGpu) { + if (options.enableGpu) { if (this._shouldLoadWebgl()) { this._enableWebglRenderer(); } else if (this._shouldLoadCanvas()) { @@ -346,18 +349,15 @@ export class XtermTerminal extends DisposableStore implements IXtermTerminal, II this._suggestAddon?.setContainer(container); - this._container = container; + this._attached = { container, options }; // Screen must be created at this point as xterm.open is called - return this._container.querySelector('.xterm-screen')!; + return this._attached?.container.querySelector('.xterm-screen')!; } private _setFocused(isFocused: boolean) { - if (isFocused !== this.isFocused) { - this.isFocused = isFocused; - this._onDidChangeFocus.fire(isFocused); - this._anyTerminalFocusContextKey.set(isFocused); - this._anyFocusedTerminalHasSelection.set(isFocused && this.raw.hasSelection()); - } + this._onDidChangeFocus.fire(isFocused); + this._anyTerminalFocusContextKey.set(isFocused); + this._anyFocusedTerminalHasSelection.set(isFocused && this.raw.hasSelection()); } write(data: string | Uint8Array): void { @@ -388,14 +388,16 @@ export class XtermTerminal extends DisposableStore implements IXtermTerminal, II this.raw.options.wordSeparator = config.wordSeparators; this.raw.options.customGlyphs = config.customGlyphs; this.raw.options.smoothScrollDuration = config.smoothScrolling ? RenderConstants.SmoothScrollDuration : 0; - if (this._shouldLoadWebgl()) { - this._enableWebglRenderer(); - } else { - this._disposeOfWebglRenderer(); - if (this._shouldLoadCanvas()) { - this._enableCanvasRenderer(); + if (this._attached?.options.enableGpu) { + if (this._shouldLoadWebgl()) { + this._enableWebglRenderer(); } else { - this._disposeOfCanvasRenderer(); + this._disposeOfWebglRenderer(); + if (this._shouldLoadCanvas()) { + this._enableCanvasRenderer(); + } else { + this._disposeOfCanvasRenderer(); + } } } this._refreshImageAddon(); @@ -567,6 +569,7 @@ export class XtermTerminal extends DisposableStore implements IXtermTerminal, II } selectAll(): void { + this.raw.focus(); this.raw.selectAll(); } @@ -596,10 +599,6 @@ export class XtermTerminal extends DisposableStore implements IXtermTerminal, II } } - setReadonly(): void { - this.raw.attachCustomKeyEventHandler(() => false); - } - private _setCursorBlink(blink: boolean): void { if (this.raw.options.cursorBlink !== blink) { this.raw.options.cursorBlink = blink; diff --git a/src/vs/workbench/contrib/terminal/common/terminalContextKey.ts b/src/vs/workbench/contrib/terminal/common/terminalContextKey.ts index 8e05935d10496..1d480dbd6499b 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalContextKey.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalContextKey.ts @@ -99,7 +99,7 @@ export namespace TerminalContextKeys { /** Whether text is selected in the active terminal. */ export const textSelected = new RawContextKey(TerminalContextKeyStrings.TextSelected, false, localize('terminalTextSelectedContextKey', "Whether text is selected in the active terminal.")); - /** Whether text is selected in a focused terminal. Focused terminals are ones active in a terminal view or an editor, where a focused terminal simply has DOM focus. */ + /** Whether text is selected in a focused terminal. `textSelected` counts text selected in an active in a terminal view or an editor, where `textSelectedInFocused` simply counts text in an element with DOM focus. */ export const textSelectedInFocused = new RawContextKey(TerminalContextKeyStrings.TextSelectedInFocused, false, localize('terminalTextSelectedInFocusedContextKey', "Whether text is selected in a focused terminal.")); /** Whether text is NOT selected in the active terminal. */ diff --git a/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts b/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts index 764f8eaa7ab35..6cab070f220ec 100644 --- a/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts +++ b/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts @@ -43,7 +43,7 @@ import { IEditor, IEditorContribution, ScrollType } from 'vs/editor/common/edito import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { IResolvedTextEditorModel, ITextModelService } from 'vs/editor/common/services/resolverService'; import { MarkdownRenderer } from 'vs/editor/contrib/markdownRenderer/browser/markdownRenderer'; -import { IPeekViewService, PeekViewWidget, peekViewTitleForeground, peekViewTitleInfoForeground } from 'vs/editor/contrib/peekView/browser/peekView'; +import { IPeekViewService, PeekViewWidget, peekViewResultsBackground, peekViewTitleForeground, peekViewTitleInfoForeground } from 'vs/editor/contrib/peekView/browser/peekView'; import { localize } from 'vs/nls'; import { Categories } from 'vs/platform/action/common/actionCommonCategories'; import { MenuEntryActionViewItem, createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; @@ -64,9 +64,11 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IColorTheme, IThemeService } from 'vs/platform/theme/common/themeService'; import { IViewPaneOptions, ViewPane } from 'vs/workbench/browser/parts/views/viewPane'; import { EditorModel } from 'vs/workbench/common/editor/editorModel'; -import { IViewDescriptorService, IViewsService } from 'vs/workbench/common/views'; -import { ITerminalService, IXtermTerminal } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { PANEL_BACKGROUND, SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; +import { IViewDescriptorService, IViewsService, ViewContainerLocation } from 'vs/workbench/common/views'; +import { IDetachedXtermTerminal, ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; import { getXtermScaledDimensions } from 'vs/workbench/contrib/terminal/browser/xterm/xtermTerminal'; +import { TERMINAL_BACKGROUND_COLOR } from 'vs/workbench/contrib/terminal/common/terminalColorRegistry'; import { flatTestItemDelimiter } from 'vs/workbench/contrib/testing/browser/explorerProjections/display'; import { getTestItemContextOverlay } from 'vs/workbench/contrib/testing/browser/explorerProjections/testItemContextOverlay'; import * as icons from 'vs/workbench/contrib/testing/browser/icons'; @@ -720,11 +722,12 @@ class TestResultsViewContent extends Disposable { this.splitView = new SplitView(containerElement, { orientation: Orientation.HORIZONTAL }); const { historyVisible, showRevealLocationOnMessages } = this.options; + const isInPeekView = this.editor !== undefined; const messageContainer = dom.append(containerElement, dom.$('.test-output-peek-message-container')); this.contentProviders = [ this._register(this.instantiationService.createInstance(DiffContentProvider, this.editor, messageContainer)), this._register(this.instantiationService.createInstance(MarkdownTestMessagePeek, messageContainer)), - this._register(this.instantiationService.createInstance(PlainTextMessagePeek, messageContainer)), + this._register(this.instantiationService.createInstance(PlainTextMessagePeek, messageContainer, isInPeekView)), ]; const treeContainer = dom.append(containerElement, dom.$('.test-output-peek-tree')); @@ -1145,33 +1148,55 @@ class PlainTextMessagePeek extends Disposable implements IPeekOutputRenderer { private dimensions?: dom.IDimension; /** Active terminal instance. */ - private readonly terminal = this._register(new MutableDisposable()); + private readonly terminal = this._register(new MutableDisposable()); /** Listener for streaming result data */ private readonly outputDataListener = this._register(new MutableDisposable()); constructor( private readonly container: HTMLElement, + private readonly isInPeekView: boolean, @ITestResultService private readonly resultService: ITestResultService, @ITerminalService private readonly terminalService: ITerminalService, + @IViewDescriptorService private readonly viewDescriptorService: IViewDescriptorService, ) { super(); } private async makeTerminal() { - if (this.terminal.value) { - this.terminal.value.clearBuffer(); - this.terminal.value.clearSearchDecorations(); - return this.terminal.value; - } - - const terminal = this.terminal.value = await this.terminalService.createDetachedXterm({ rows: 10, cols: 80 }); - terminal.setReadonly(); - return terminal; + const prev = this.terminal.value; + if (prev) { + prev.clearBuffer(); + prev.clearSearchDecorations(); + // clearBuffer tries to retain the prompt line, but this doesn't exist for tests. + // So clear the screen (J) and move to home (H) to ensure previous data is cleaned up. + prev.write(`\x1b[2J\x1b[0;0H`); + return prev; + } + + return this.terminal.value = await this.terminalService.createDetachedXterm({ + rows: 10, + cols: 80, + readonly: true, + colorProvider: { + getBackgroundColor: theme => { + const terminalBackground = theme.getColor(TERMINAL_BACKGROUND_COLOR); + if (terminalBackground) { + return terminalBackground; + } + if (this.isInPeekView) { + return theme.getColor(peekViewResultsBackground); + } + const location = this.viewDescriptorService.getViewLocationById(Testing.ResultsViewId); + return location === ViewContainerLocation.Panel + ? theme.getColor(PANEL_BACKGROUND) + : theme.getColor(SIDE_BAR_BACKGROUND); + }, + } + }); } public async update(subject: InspectSubject) { this.outputDataListener.clear(); - this.terminal.value?.clearBuffer(); if (subject instanceof MessageSubject) { const message = subject.messages[subject.messageIndex]; @@ -1208,11 +1233,11 @@ class PlainTextMessagePeek extends Disposable implements IPeekOutputRenderer { } } - private writeNotice(terminal: IXtermTerminal, str: string) { - terminal.write(`\x1b[1m${str}\x1b[0m`); + private writeNotice(terminal: IDetachedXtermTerminal, str: string) { + terminal.write(`\x1b[2m${str}\x1b[0m`); } - private attachTerminalToDom(terminal: IXtermTerminal) { + private attachTerminalToDom(terminal: IDetachedXtermTerminal) { terminal.write('\x1b[?25l'); // hide cursor requestAnimationFrame(() => this.layoutTerminal(terminal)); this.container.classList.add('xterm-detached-instance'); @@ -1232,7 +1257,7 @@ class PlainTextMessagePeek extends Disposable implements IPeekOutputRenderer { } private layoutTerminal( - xterm: IXtermTerminal, + xterm: IDetachedXtermTerminal, width = this.dimensions?.width ?? this.container.clientWidth, height = this.dimensions?.height ?? this.container.clientHeight ) { From 7cc847f61a686c169cd5abdfbdb71feaf7c4143f Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Tue, 13 Jun 2023 13:22:15 -0700 Subject: [PATCH 05/10] add cwd capability to test peek view --- .../common/capabilities/capabilities.ts | 8 ----- .../contrib/terminal/browser/terminal.ts | 1 + .../terminal/browser/terminalService.ts | 4 +-- .../testing/browser/testingOutputPeek.ts | 30 +++++++++++++++++++ 4 files changed, 33 insertions(+), 10 deletions(-) diff --git a/src/vs/platform/terminal/common/capabilities/capabilities.ts b/src/vs/platform/terminal/common/capabilities/capabilities.ts index fb46fd94a56dd..552b181163966 100644 --- a/src/vs/platform/terminal/common/capabilities/capabilities.ts +++ b/src/vs/platform/terminal/common/capabilities/capabilities.ts @@ -102,14 +102,6 @@ export interface ITerminalCapabilityStore { get(capability: T): ITerminalCapabilityImplMap[T] | undefined; } -export const emptyTerminalCapabilityStore: ITerminalCapabilityStore = { - items: [][Symbol.iterator](), - onDidAddCapability: Event.None, - onDidRemoveCapability: Event.None, - has: () => false, - get: () => undefined -}; - /** * Maps capability types to their implementation, enabling strongly typed fetching of * implementations. diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.ts b/src/vs/workbench/contrib/terminal/browser/terminal.ts index 2284cf96bb500..f1a807b3bfc86 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.ts @@ -142,6 +142,7 @@ export interface IDetachedXTermOptions { cols: number; rows: number; colorProvider: IXtermColorProvider; + capabilities?: ITerminalCapabilityStore; readonly?: boolean; } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalService.ts b/src/vs/workbench/contrib/terminal/browser/terminalService.ts index 1dec82d52c41f..53932e7ec962d 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalService.ts @@ -50,7 +50,7 @@ import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteA import { XtermTerminal } from 'vs/workbench/contrib/terminal/browser/xterm/xtermTerminal'; import { TerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminalInstance'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { emptyTerminalCapabilityStore } from 'vs/platform/terminal/common/capabilities/capabilities'; +import { TerminalCapabilityStore } from 'vs/platform/terminal/common/capabilities/terminalCapabilityStore'; export class TerminalService implements ITerminalService { declare _serviceBrand: undefined; @@ -989,7 +989,7 @@ export class TerminalService implements ITerminalService { options.cols, options.rows, options.colorProvider, - emptyTerminalCapabilityStore, + options.capabilities || new TerminalCapabilityStore(), '', undefined, false, diff --git a/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts b/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts index 6cab070f220ec..107265111f283 100644 --- a/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts +++ b/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts @@ -61,7 +61,10 @@ import { INotificationService } from 'vs/platform/notification/common/notificati import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { TerminalCapability } from 'vs/platform/terminal/common/capabilities/capabilities'; +import { TerminalCapabilityStore } from 'vs/platform/terminal/common/capabilities/terminalCapabilityStore'; import { IColorTheme, IThemeService } from 'vs/platform/theme/common/themeService'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IViewPaneOptions, ViewPane } from 'vs/workbench/browser/parts/views/viewPane'; import { EditorModel } from 'vs/workbench/common/editor/editorModel'; import { PANEL_BACKGROUND, SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; @@ -1146,6 +1149,7 @@ class MarkdownTestMessagePeek extends Disposable implements IPeekOutputRenderer class PlainTextMessagePeek extends Disposable implements IPeekOutputRenderer { private dimensions?: dom.IDimension; + private readonly terminalCwd = this._register(new MutableObservableValue('')); /** Active terminal instance. */ private readonly terminal = this._register(new MutableDisposable()); @@ -1158,6 +1162,7 @@ class PlainTextMessagePeek extends Disposable implements IPeekOutputRenderer { @ITestResultService private readonly resultService: ITestResultService, @ITerminalService private readonly terminalService: ITerminalService, @IViewDescriptorService private readonly viewDescriptorService: IViewDescriptorService, + @IWorkspaceContextService private readonly workspaceContext: IWorkspaceContextService, ) { super(); } @@ -1173,10 +1178,21 @@ class PlainTextMessagePeek extends Disposable implements IPeekOutputRenderer { return prev; } + const capabilities = new TerminalCapabilityStore(); + const cwd = this.terminalCwd; + capabilities.add(TerminalCapability.CwdDetection, { + type: TerminalCapability.CwdDetection, + get cwds() { return [cwd.value]; }, + onDidChangeCwd: cwd.onDidChange, + getCwd: () => cwd.value, + updateCwd: () => { }, + }); + return this.terminal.value = await this.terminalService.createDetachedXterm({ rows: 10, cols: 80, readonly: true, + capabilities, colorProvider: { getBackgroundColor: theme => { const terminalBackground = theme.getColor(TERMINAL_BACKGROUND_COLOR); @@ -1203,6 +1219,8 @@ class PlainTextMessagePeek extends Disposable implements IPeekOutputRenderer { if (isDiffable(message) || typeof message.message !== 'string') { return this.clear(); } + + this.updateCwd(subject.test.uri); const terminal = await this.makeTerminal(); terminal.write(message.message); this.layoutTerminal(terminal); @@ -1214,6 +1232,10 @@ class PlainTextMessagePeek extends Disposable implements IPeekOutputRenderer { return this.clear(); } + // Update the cwd and use the first test to try to hint at the correct cwd, + // but often this will fall back to the first workspace folder. + this.updateCwd(Iterable.find(result.tests, t => !!t.item.uri)?.item.uri); + const terminal = await this.makeTerminal(); if (result instanceof LiveTestResult) { let hadData = false; @@ -1233,6 +1255,14 @@ class PlainTextMessagePeek extends Disposable implements IPeekOutputRenderer { } } + private updateCwd(testUri?: URI) { + const wf = (testUri && this.workspaceContext.getWorkspaceFolder(testUri)) + || this.workspaceContext.getWorkspace().folders[0]; + if (wf) { + this.terminalCwd.value = wf.uri.fsPath; + } + } + private writeNotice(terminal: IDetachedXtermTerminal, str: string) { terminal.write(`\x1b[2m${str}\x1b[0m`); } From e47fe293fb3a83e5e488eeb3870c791df2513ad7 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Tue, 13 Jun 2023 15:47:12 -0700 Subject: [PATCH 06/10] add padding and fix peek display issues --- .../terminal/browser/media/terminal.css | 34 +++++++++---------- .../terminal/browser/terminalEditor.ts | 2 +- .../contrib/testing/browser/media/testing.css | 4 +++ .../testing/browser/testingOutputPeek.ts | 3 +- 4 files changed, 23 insertions(+), 20 deletions(-) diff --git a/src/vs/workbench/contrib/terminal/browser/media/terminal.css b/src/vs/workbench/contrib/terminal/browser/media/terminal.css index 684ebaa25778f..6932f80a89fe6 100644 --- a/src/vs/workbench/contrib/terminal/browser/media/terminal.css +++ b/src/vs/workbench/contrib/terminal/browser/media/terminal.css @@ -26,8 +26,8 @@ .monaco-workbench .pane-body.integrated-terminal .terminal-groups-container, .monaco-workbench .pane-body.integrated-terminal .terminal-group, .monaco-workbench .pane-body.integrated-terminal .terminal-split-pane, -.monaco-workbench .editor-instance .terminal-split-pane, -.monaco-workbench .editor-instance .terminal-outer-container { +.monaco-workbench .terminal-editor .terminal-split-pane, +.monaco-workbench .terminal-editor .terminal-outer-container { height: 100%; } .monaco-workbench .part.sidebar .pane-body.integrated-terminal .terminal-outer-container, @@ -48,7 +48,7 @@ background-color: var(--vscode-terminal-tab-activeBorder); } /* Override monaco's styles for terminal editors */ -.monaco-workbench .editor-instance .xterm textarea:focus { +.monaco-workbench .terminal-editor .xterm textarea:focus { opacity: 0 !important; outline: 0 !important; } @@ -62,17 +62,17 @@ background-image: none !important; } -.monaco-workbench .editor-instance .terminal-wrapper { +.monaco-workbench .terminal-editor .terminal-wrapper { background-color: var(--vscode-terminal-background, --vscode-editorPane-background); } -.monaco-workbench .editor-instance .terminal-wrapper, +.monaco-workbench .terminal-editor .terminal-wrapper, .monaco-workbench .pane-body.integrated-terminal .terminal-wrapper { display: block; height: 100%; box-sizing: border-box; } -.monaco-workbench .editor-instance .xterm, +.monaco-workbench .terminal-editor .xterm, .monaco-workbench .pane-body.integrated-terminal .xterm { /* All terminals have at least 10px left/right edge padding and 2 padding on the bottom (so underscores on last line are visible */ padding: 0 10px 2px; @@ -88,23 +88,23 @@ top: 0; } -.monaco-workbench .editor-instance .terminal-wrapper.fixed-dims .xterm, +.monaco-workbench .terminal-editor .terminal-wrapper.fixed-dims .xterm, .monaco-workbench .pane-body.integrated-terminal .terminal-wrapper.fixed-dims .xterm { position: static; } -.monaco-workbench .editor-instance .xterm-viewport, +.monaco-workbench .terminal-editor .xterm-viewport, .monaco-workbench .pane-body.integrated-terminal .xterm-viewport { z-index: 30; } -.monaco-workbench .editor-instance .xterm-decoration-overview-ruler, +.monaco-workbench .terminal-editor .xterm-decoration-overview-ruler, .monaco-workbench .pane-body.integrated-terminal .xterm-decoration-overview-ruler { z-index: 31; /* Must be higher than .xterm-viewport */ pointer-events: none; } -.monaco-workbench .editor-instance .xterm-screen, +.monaco-workbench .terminal-editor .xterm-screen, .monaco-workbench .pane-body.integrated-terminal .xterm-screen { z-index: 31; } @@ -127,7 +127,7 @@ .xterm.xterm-cursor-pointer .xterm-screen { cursor: pointer; } .xterm.column-select.focus .xterm-screen { cursor: crosshair; } -.monaco-workbench .editor-instance .xterm { +.monaco-workbench .terminal-editor .xterm { padding-left: 20px !important; } @@ -136,34 +136,34 @@ padding-left: 20px !important; } -.monaco-workbench .editor-instance .terminal-group .monaco-split-view2.horizontal .split-view-view:last-child .xterm, +.monaco-workbench .terminal-editor .terminal-group .monaco-split-view2.horizontal .split-view-view:last-child .xterm, .monaco-workbench .pane-body.integrated-terminal .terminal-group .monaco-split-view2.horizontal .split-view-view:last-child .xterm { padding-right: 20px; } -.monaco-workbench .editor-instance .xterm a:not(.xterm-invalid-link), +.monaco-workbench .terminal-editor .xterm a:not(.xterm-invalid-link), .monaco-workbench .pane-body.integrated-terminal .xterm a:not(.xterm-invalid-link) { /* To support message box sizing */ position: relative; } -.monaco-workbench .editor-instance .terminal-wrapper > div, +.monaco-workbench .terminal-editor .terminal-wrapper > div, .monaco-workbench .pane-body.integrated-terminal .terminal-wrapper > div { height: 100%; } -.monaco-workbench .editor-instance .xterm-viewport, +.monaco-workbench .terminal-editor .xterm-viewport, .monaco-workbench .pane-body.integrated-terminal .xterm-viewport { box-sizing: border-box; } -.monaco-workbench .editor-instance .terminal-wrapper.fixed-dims, +.monaco-workbench .terminal-editor .terminal-wrapper.fixed-dims, .monaco-workbench .pane-body.integrated-terminal .terminal-wrapper.fixed-dims { /* The viewport should be positioned against this so it does't conflict with a fixed dimensions terminal horizontal scroll bar*/ position: relative; } -.monaco-workbench .editor-instance .terminal-wrapper:not(.fixed-dims) .xterm-viewport, +.monaco-workbench .terminal-editor .terminal-wrapper:not(.fixed-dims) .xterm-viewport, .monaco-workbench .pane-body.integrated-terminal .terminal-wrapper:not(.fixed-dims) .xterm-viewport { /* Override xterm.js' width as we want to size the viewport to fill the panel so the scrollbar is on the right edge */ width: auto !important; diff --git a/src/vs/workbench/contrib/terminal/browser/terminalEditor.ts b/src/vs/workbench/contrib/terminal/browser/terminalEditor.ts index 9ddc5d6240384..eb04b5e266766 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalEditor.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalEditor.ts @@ -103,7 +103,7 @@ export class TerminalEditor extends EditorPane { // eslint-disable-next-line @typescript-eslint/naming-convention protected createEditor(parent: HTMLElement): void { this._editorInstanceElement = parent; - this._overflowGuardElement = dom.$('.terminal-overflow-guard'); + this._overflowGuardElement = dom.$('.terminal-overflow-guard.terminal-editor'); this._editorInstanceElement.appendChild(this._overflowGuardElement); this._registerListeners(); } diff --git a/src/vs/workbench/contrib/testing/browser/media/testing.css b/src/vs/workbench/contrib/testing/browser/media/testing.css index 11ae1f8784f42..e468c76107827 100644 --- a/src/vs/workbench/contrib/testing/browser/media/testing.css +++ b/src/vs/workbench/contrib/testing/browser/media/testing.css @@ -191,6 +191,10 @@ height: 100%; } +.test-output-peek-message-container > .terminal { + margin: 4px; +} + .monaco-editor .zone-widget.test-output-peek .preview-text { padding: 8px 12px 8px 20px; height: calc(100% - 16px); diff --git a/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts b/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts index 107265111f283..4977c6095afb5 100644 --- a/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts +++ b/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts @@ -1270,7 +1270,6 @@ class PlainTextMessagePeek extends Disposable implements IPeekOutputRenderer { private attachTerminalToDom(terminal: IDetachedXtermTerminal) { terminal.write('\x1b[?25l'); // hide cursor requestAnimationFrame(() => this.layoutTerminal(terminal)); - this.container.classList.add('xterm-detached-instance'); terminal.attachToElement(this.container, { enableGpu: false }); } @@ -1291,7 +1290,7 @@ class PlainTextMessagePeek extends Disposable implements IPeekOutputRenderer { width = this.dimensions?.width ?? this.container.clientWidth, height = this.dimensions?.height ?? this.container.clientHeight ) { - width -= 10; // scrollbar width + width -= 10 + 8; // scrollbar width + margin const scaled = getXtermScaledDimensions(xterm.getFont(), width, height); if (scaled) { xterm.resize(scaled.cols, scaled.rows); From a3fa87894d8504f7f3e6500a6d9119580c640779 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Tue, 13 Jun 2023 16:05:40 -0700 Subject: [PATCH 07/10] ignore scroll events with default prevented --- src/vs/base/browser/ui/scrollbar/scrollableElement.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/vs/base/browser/ui/scrollbar/scrollableElement.ts b/src/vs/base/browser/ui/scrollbar/scrollableElement.ts index 87d4ed4d9fa38..5cf11ce785447 100644 --- a/src/vs/base/browser/ui/scrollbar/scrollableElement.ts +++ b/src/vs/base/browser/ui/scrollbar/scrollableElement.ts @@ -373,6 +373,9 @@ export abstract class AbstractScrollableElement extends Widget { } private _onMouseWheel(e: StandardWheelEvent): void { + if (e.browserEvent?.defaultPrevented) { + return; + } const classifier = MouseWheelClassifier.INSTANCE; if (SCROLL_WHEEL_SMOOTH_SCROLL_ENABLED) { From 1f3b6d67c2468cb08fa6ad7f3261dc320763b148 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Thu, 15 Jun 2023 14:20:21 -0700 Subject: [PATCH 08/10] fixup tests --- .../contrib/terminal/test/browser/xterm/xtermTerminal.test.ts | 4 +++- .../accessibility/test/browser/bufferContentTracker.test.ts | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/terminal/test/browser/xterm/xtermTerminal.test.ts b/src/vs/workbench/contrib/terminal/test/browser/xterm/xtermTerminal.test.ts index a9c5ff1af01dd..f3e30380a6027 100644 --- a/src/vs/workbench/contrib/terminal/test/browser/xterm/xtermTerminal.test.ts +++ b/src/vs/workbench/contrib/terminal/test/browser/xterm/xtermTerminal.test.ts @@ -30,6 +30,7 @@ import { TestLifecycleService } from 'vs/workbench/test/browser/workbenchTestSer import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; import { Color, RGBA } from 'vs/base/common/color'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; class TestWebglAddon implements WebglAddon { static shouldThrow = false; @@ -117,6 +118,7 @@ suite('XtermTerminal', () => { instantiationService.stub(IViewDescriptorService, viewDescriptorService); instantiationService.stub(IContextMenuService, instantiationService.createInstance(ContextMenuService)); instantiationService.stub(ILifecycleService, new TestLifecycleService()); + instantiationService.stub(IContextKeyService, new MockContextKeyService()); configHelper = instantiationService.createInstance(TerminalConfigHelper); xterm = instantiationService.createInstance(TestXtermTerminal, Terminal, configHelper, 80, 30, { getBackgroundColor: () => undefined }, new TerminalCapabilityStore(), '', new MockContextKeyService().createKey('', true)!, true); @@ -251,7 +253,7 @@ suite('XtermTerminal', () => { // Open xterm as otherwise the webgl addon won't activate const container = document.createElement('div'); - xterm.raw.open(container); + xterm.attachToElement(container); // Auto should activate the webgl addon await configurationService.setUserConfiguration('terminal', { integrated: { ...defaultTerminalConfig, gpuAcceleration: 'auto' } }); diff --git a/src/vs/workbench/contrib/terminalContrib/accessibility/test/browser/bufferContentTracker.test.ts b/src/vs/workbench/contrib/terminalContrib/accessibility/test/browser/bufferContentTracker.test.ts index 8ee82c2813db6..e1945415abf32 100644 --- a/src/vs/workbench/contrib/terminalContrib/accessibility/test/browser/bufferContentTracker.test.ts +++ b/src/vs/workbench/contrib/terminalContrib/accessibility/test/browser/bufferContentTracker.test.ts @@ -7,6 +7,7 @@ import * as assert from 'assert'; import { isWindows } from 'vs/base/common/platform'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { ContextMenuService } from 'vs/platform/contextview/browser/contextMenuService'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; @@ -56,6 +57,7 @@ suite('Buffer Content Tracker', () => { instantiationService.stub(ILogService, new NullLogService()); instantiationService.stub(IContextMenuService, instantiationService.createInstance(ContextMenuService)); instantiationService.stub(ILifecycleService, new TestLifecycleService()); + instantiationService.stub(IContextKeyService, new MockContextKeyService()); configHelper = instantiationService.createInstance(TerminalConfigHelper); capabilities = new TerminalCapabilityStore(); if (!isWindows) { From 8a63e7b573cce88fa0f5076f3f309ac61abcc021 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Thu, 15 Jun 2023 15:11:06 -0700 Subject: [PATCH 09/10] comments --- .../terminal/browser/media/terminal.css | 21 +++++++++------- .../contrib/terminal/browser/terminal.ts | 3 +-- .../terminal/browser/terminalActions.ts | 2 +- .../terminal/browser/terminalService.ts | 10 ++++---- .../contrib/testing/browser/media/testing.css | 4 ---- .../testing/browser/testingOutputPeek.ts | 24 +------------------ 6 files changed, 20 insertions(+), 44 deletions(-) diff --git a/src/vs/workbench/contrib/terminal/browser/media/terminal.css b/src/vs/workbench/contrib/terminal/browser/media/terminal.css index 6932f80a89fe6..814626ed4f5b1 100644 --- a/src/vs/workbench/contrib/terminal/browser/media/terminal.css +++ b/src/vs/workbench/contrib/terminal/browser/media/terminal.css @@ -72,10 +72,13 @@ box-sizing: border-box; } -.monaco-workbench .terminal-editor .xterm, -.monaco-workbench .pane-body.integrated-terminal .xterm { +.monaco-workbench .xterm { /* All terminals have at least 10px left/right edge padding and 2 padding on the bottom (so underscores on last line are visible */ padding: 0 10px 2px; +} + +.monaco-workbench .terminal-editor .xterm, +.monaco-workbench .pane-body.integrated-terminal .xterm { /* Bottom align the terminal within the split pane */ position: absolute; bottom: 0; @@ -523,25 +526,25 @@ } .force-scrollbar .xterm .xterm-viewport, -.xterm.focus .xterm-viewport, -.xterm:focus .xterm-viewport, -.xterm:hover .xterm-viewport { +.monaco-workbench .xterm.focus .xterm-viewport, +.monaco-workbench .xterm:focus .xterm-viewport, +.monaco-workbench .xterm:hover .xterm-viewport { background-color: var(--vscode-scrollbarSlider-background) !important; } -.xterm-viewport { +.monaco-workbench .xterm-viewport { scrollbar-color: var(--vscode-scrollbarSlider-background) transparent; } -.xterm .xterm-viewport::-webkit-scrollbar-thumb:hover { +.monaco-workbench .xterm .xterm-viewport::-webkit-scrollbar-thumb:hover { background-color: var(--vscode-scrollbarSlider-hoverBackground); } -.xterm-viewport:hover { +.monaco-workbench .xterm-viewport:hover { scrollbar-color: var(--vscode-scrollbarSlider-hoverBackground) transparent; } -.xterm .xterm-viewport::-webkit-scrollbar-thumb:active { +.monaco-workbench .xterm .xterm-viewport::-webkit-scrollbar-thumb:active { background-color: var(--vscode-scrollbarSlider-activeBackground); } diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.ts b/src/vs/workbench/contrib/terminal/browser/terminal.ts index 97491cb642a0e..057b90e4e98e7 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.ts @@ -153,7 +153,7 @@ export interface ITerminalService extends ITerminalInstanceHost { /** Gets all terminal instances, including editor and terminal view (group) instances. */ readonly instances: readonly ITerminalInstance[]; /** Gets detached terminal instances created via {@link createDetachedXterm}. */ - readonly detachedInstances: Iterable; + readonly detachedXterms: Iterable; configHelper: ITerminalConfigHelper; isProcessSupportRegistered: boolean; readonly connectionState: TerminalConnectionState; @@ -1030,7 +1030,6 @@ export interface IXtermTerminal extends IDisposable { */ getFont(): ITerminalFont; - /** Scroll the terminal buffer down 1 line. */ scrollDownLine(): void; /** Scroll the terminal buffer down 1 page. */ scrollDownPage(): void; /** Scroll the terminal buffer to the bottom. */ scrollToBottom(): void; diff --git a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts index d74a174d111df..8dcc05f1f4e12 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts @@ -194,7 +194,7 @@ export function registerActiveXtermAction( return registerTerminalAction({ ...options, run: (c, accessor, args) => { - const activeDetached = Iterable.find(c.service.detachedInstances, d => d.isFocused); + const activeDetached = Iterable.find(c.service.detachedXterms, d => d.isFocused); if (activeDetached) { return originalRun(activeDetached as XtermTerminal, accessor, undefined, args); } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalService.ts b/src/vs/workbench/contrib/terminal/browser/terminalService.ts index 53932e7ec962d..08dd70e1fad31 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalService.ts @@ -57,7 +57,7 @@ export class TerminalService implements ITerminalService { private _hostActiveTerminals: Map = new Map(); - private _detachedInstances = new Set(); + private _detachedXterms = new Set(); private _terminalEditorActive: IContextKey; private readonly _terminalShellTypeContextKey: IContextKey; @@ -85,8 +85,8 @@ export class TerminalService implements ITerminalService { get instances(): ITerminalInstance[] { return this._terminalGroupService.instances.concat(this._terminalEditorService.instances); } - get detachedInstances(): Iterable { - return this._detachedInstances; + get detachedXterms(): Iterable { + return this._detachedXterms; } private _reconnectedTerminals: Map = new Map(); @@ -999,8 +999,8 @@ export class TerminalService implements ITerminalService { instance.raw.attachCustomKeyEventHandler(() => false); } - this._detachedInstances.add(instance); - instance.onDidDispose(() => this._detachedInstances.delete(instance)); + this._detachedXterms.add(instance); + instance.onDidDispose(() => this._detachedXterms.delete(instance)); return instance; } diff --git a/src/vs/workbench/contrib/testing/browser/media/testing.css b/src/vs/workbench/contrib/testing/browser/media/testing.css index e468c76107827..11ae1f8784f42 100644 --- a/src/vs/workbench/contrib/testing/browser/media/testing.css +++ b/src/vs/workbench/contrib/testing/browser/media/testing.css @@ -191,10 +191,6 @@ height: 100%; } -.test-output-peek-message-container > .terminal { - margin: 4px; -} - .monaco-editor .zone-widget.test-output-peek .preview-text { padding: 8px 12px 8px 20px; height: calc(100% - 16px); diff --git a/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts b/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts index 4977c6095afb5..5fc25be8ed2ae 100644 --- a/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts +++ b/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts @@ -1290,7 +1290,7 @@ class PlainTextMessagePeek extends Disposable implements IPeekOutputRenderer { width = this.dimensions?.width ?? this.container.clientWidth, height = this.dimensions?.height ?? this.container.clientHeight ) { - width -= 10 + 8; // scrollbar width + margin + width -= 10 + 20; // scrollbar width + margin const scaled = getXtermScaledDimensions(xterm.getFont(), width, height); if (scaled) { xterm.resize(scaled.cols, scaled.rows); @@ -2139,28 +2139,6 @@ export class ToggleTestingPeekHistory extends Action2 { } } -// export class CopyTestOutputSelection extends Action2 { -// public static readonly ID = 'testing.copyTestOutputSelection'; - -// constructor() { -// super({ -// id: ToggleTestingPeekHistory.ID, -// title: { value: localize('workbench.action.terminal.copySelection', "Copy Selection"), original: 'Copy Selection' }, -// keybinding: [{ -// primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyC, -// mac: { primary: KeyMod.CtrlCmd | KeyCode.KeyC }, -// weight: KeybindingWeight.WorkbenchContrib, -// when: ContextKeyExpr.and(TerminalContextKeys.textSelected, TerminalContextKeys.focus) -// }], -// }); -// } - -// public override run(accessor: ServicesAccessor) { -// const opener = accessor.get(ITestingPeekOpener); -// opener.historyVisible.value = !opener.historyVisible.value; -// } -// } - class CreationCache { private readonly v = new WeakMap(); From 66f6b55058a37a49a2d84f1a8a0bb3d2893b4697 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Thu, 15 Jun 2023 15:54:37 -0700 Subject: [PATCH 10/10] comments --- .../contrib/terminal/browser/terminal.ts | 28 +++++++++++++++++++ .../terminal/browser/terminalActions.ts | 7 ++--- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.ts b/src/vs/workbench/contrib/terminal/browser/terminal.ts index 057b90e4e98e7..21216688f4d2a 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.ts @@ -1030,6 +1030,34 @@ export interface IXtermTerminal extends IDisposable { */ getFont(): ITerminalFont; + /** + * Gets whether there's any terminal selection. + */ + hasSelection(): boolean; + + /** + * Clears any terminal selection. + */ + clearSelection(): void; + + /** + * Selects all terminal contents/ + */ + selectAll(): void; + + /** + * Copies the terminal selection. + * @param {boolean} copyAsHtml Whether to copy selection as HTML, defaults to false. + */ + copySelection(copyAsHtml?: boolean): void; + + /** + * Focuses the terminal. Warning: {@link ITerminalInstance.focus} should be + * preferred when dealing with terminal instances in order to get + * accessibility triggers. + */ + focus(): void; + /** Scroll the terminal buffer down 1 line. */ scrollDownLine(): void; /** Scroll the terminal buffer down 1 page. */ scrollDownPage(): void; /** Scroll the terminal buffer to the bottom. */ scrollToBottom(): void; diff --git a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts index 8dcc05f1f4e12..6b27e824491dc 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts @@ -32,7 +32,7 @@ import { IWorkspaceContextService, IWorkspaceFolder } from 'vs/platform/workspac import { PICK_WORKSPACE_FOLDER_COMMAND_ID } from 'vs/workbench/browser/actions/workspaceCommands'; import { CLOSE_EDITOR_COMMAND_ID } from 'vs/workbench/browser/parts/editor/editorCommands'; import { ResourceContextKey } from 'vs/workbench/common/contextkeys'; -import { Direction, ICreateTerminalOptions, ITerminalEditorService, ITerminalGroupService, ITerminalInstance, ITerminalInstanceService, ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { Direction, ICreateTerminalOptions, ITerminalEditorService, ITerminalGroupService, ITerminalInstance, ITerminalInstanceService, ITerminalService, IXtermTerminal } from 'vs/workbench/contrib/terminal/browser/terminal'; import { TerminalQuickAccessProvider } from 'vs/workbench/contrib/terminal/browser/terminalQuickAccess'; import { IRemoteTerminalAttachTarget, ITerminalConfigHelper, ITerminalProfileResolverService, ITerminalProfileService, TerminalCommandId } from 'vs/workbench/contrib/terminal/common/terminal'; import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey'; @@ -60,7 +60,6 @@ import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService import { TerminalCapability } from 'vs/platform/terminal/common/capabilities/capabilities'; import { killTerminalIcon, newTerminalIcon } from 'vs/workbench/contrib/terminal/browser/terminalIcons'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; -import { XtermTerminal } from 'vs/workbench/contrib/terminal/browser/xterm/xtermTerminal'; import { Iterable } from 'vs/base/common/iterator'; export const switchTerminalActionViewItemSeparator = '\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500'; @@ -188,7 +187,7 @@ export function registerActiveInstanceAction( * This includes detached xterm terminals that are not managed by an {@link ITerminalInstance}. */ export function registerActiveXtermAction( - options: IAction2Options & { run: (activeTerminal: XtermTerminal, accessor: ServicesAccessor, instance?: ITerminalInstance, args?: unknown) => void | Promise } + options: IAction2Options & { run: (activeTerminal: IXtermTerminal, accessor: ServicesAccessor, instance?: ITerminalInstance, args?: unknown) => void | Promise } ): IDisposable { const originalRun = options.run; return registerTerminalAction({ @@ -196,7 +195,7 @@ export function registerActiveXtermAction( run: (c, accessor, args) => { const activeDetached = Iterable.find(c.service.detachedXterms, d => d.isFocused); if (activeDetached) { - return originalRun(activeDetached as XtermTerminal, accessor, undefined, args); + return originalRun(activeDetached, accessor, undefined, args); } const activeInstance = c.service.activeInstance;