testing: use a real terminal in test Test Results view (#184963)

* wip

* wip

* it works

* address pr comments

* add cwd capability to test peek view

* add padding and fix peek display issues

* ignore scroll events with default prevented

* fixup tests

* comments

* comments
This commit is contained in:
Connor Peet
2023-06-15 21:47:49 -07:00
committed by GitHub
parent 1eec40969e
commit 0e9c2fa760
14 changed files with 609 additions and 260 deletions

View File

@@ -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) {

View File

@@ -3,8 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.monaco-workbench .editor-instance .xterm-viewport,
.monaco-workbench .pane-body.integrated-terminal .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;
@@ -12,45 +11,35 @@
transition: background-color 800ms linear;
}
.monaco-workbench .editor-instance .xterm-viewport,
.monaco-workbench .pane-body.integrated-terminal .xterm-viewport {
.monaco-workbench .xterm-viewport {
scrollbar-width: thin;
}
.monaco-workbench .editor-instance .xterm-viewport::-webkit-scrollbar,
.monaco-workbench .pane-body.integrated-terminal .xterm-viewport::-webkit-scrollbar {
.monaco-workbench .xterm-viewport::-webkit-scrollbar {
width: 10px;
}
.monaco-workbench .editor-instance .xterm-viewport::-webkit-scrollbar-track,
.monaco-workbench .pane-body.integrated-terminal .xterm-viewport::-webkit-scrollbar-track {
.monaco-workbench .xterm-viewport::-webkit-scrollbar-track {
opacity: 0;
}
.monaco-workbench .editor-instance .xterm-viewport::-webkit-scrollbar-thumb,
.monaco-workbench .pane-body.integrated-terminal .xterm-viewport::-webkit-scrollbar-thumb {
.monaco-workbench .xterm-viewport::-webkit-scrollbar-thumb {
min-height: 20px;
background-color: inherit;
}
.monaco-workbench .editor-instance .force-scrollbar .xterm .xterm-viewport,
.monaco-workbench .pane-body.integrated-terminal .force-scrollbar .xterm .xterm-viewport,
.monaco-workbench .editor-instance .xterm.focus .xterm-viewport,
.monaco-workbench .pane-body.integrated-terminal .xterm.focus .xterm-viewport,
.monaco-workbench .editor-instance .xterm:focus .xterm-viewport,
.monaco-workbench .pane-body.integrated-terminal .xterm:focus .xterm-viewport,
.monaco-workbench .editor-instance .xterm:hover .xterm-viewport,
.monaco-workbench .pane-body.integrated-terminal .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;
}
.monaco-workbench .editor-instance .xterm .xterm-viewport::-webkit-scrollbar-thumb:hover,
.monaco-workbench .pane-body.integrated-terminal .xterm .xterm-viewport::-webkit-scrollbar-thumb:hover {
.monaco-workbench .xterm .xterm-viewport::-webkit-scrollbar-thumb:hover {
transition: opacity 0ms linear;
}
.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 {
.monaco-workbench .xterm .xterm-viewport::-webkit-scrollbar-thumb:window-inactive {
background-color: inherit;
}

View File

@@ -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,20 +62,23 @@
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 .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;
@@ -88,23 +91,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 +130,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 +139,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;
@@ -245,7 +248,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;
}
@@ -522,34 +525,26 @@
background-color: var(--vscode-terminal-hoverHighlightBackground);
}
.monaco-workbench .editor-instance .force-scrollbar .xterm .xterm-viewport,
.monaco-workbench .pane-body.integrated-terminal .force-scrollbar .xterm .xterm-viewport,
.monaco-workbench .editor-instance .xterm.focus .xterm-viewport,
.monaco-workbench .pane-body.integrated-terminal .xterm.focus .xterm-viewport,
.monaco-workbench .editor-instance .xterm:focus .xterm-viewport,
.monaco-workbench .pane-body.integrated-terminal .xterm:focus .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,
.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;
}
.monaco-workbench .editor-instance .xterm-viewport,
.monaco-workbench .pane-body.integrated-terminal .xterm-viewport {
.monaco-workbench .xterm-viewport {
scrollbar-color: var(--vscode-scrollbarSlider-background) transparent;
}
.monaco-workbench .editor-instance .xterm .xterm-viewport::-webkit-scrollbar-thumb:hover,
.monaco-workbench .pane-body.integrated-terminal .xterm .xterm-viewport::-webkit-scrollbar-thumb:hover {
.monaco-workbench .xterm .xterm-viewport::-webkit-scrollbar-thumb:hover {
background-color: var(--vscode-scrollbarSlider-hoverBackground);
}
.monaco-workbench .editor-instance .xterm-viewport:hover,
.monaco-workbench .pane-body.integrated-terminal .xterm-viewport:hover {
.monaco-workbench .xterm-viewport:hover {
scrollbar-color: var(--vscode-scrollbarSlider-hoverBackground) transparent;
}
.monaco-workbench .editor-instance .xterm .xterm-viewport::-webkit-scrollbar-thumb:active,
.monaco-workbench .pane-body.integrated-terminal .xterm .xterm-viewport::-webkit-scrollbar-thumb:active {
.monaco-workbench .xterm .xterm-viewport::-webkit-scrollbar-thumb:active {
background-color: var(--vscode-scrollbarSlider-activeBackground);
}

View File

@@ -139,11 +139,21 @@ export const enum TerminalConnectionState {
Connected
}
export interface IDetachedXTermOptions {
cols: number;
rows: number;
colorProvider: IXtermColorProvider;
capabilities?: ITerminalCapabilityStore;
readonly?: boolean;
}
export interface ITerminalService extends ITerminalInstanceHost {
readonly _serviceBrand: undefined;
/** Gets all terminal instances, including editor and terminal view (group) instances. */
readonly instances: readonly ITerminalInstance[];
/** Gets detached terminal instances created via {@link createDetachedXterm}. */
readonly detachedXterms: Iterable<IXtermTerminal>;
configHelper: ITerminalConfigHelper;
isProcessSupportRegistered: boolean;
readonly connectionState: TerminalConnectionState;
@@ -171,6 +181,13 @@ export interface ITerminalService extends ITerminalInstanceHost {
*/
createTerminal(options?: ICreateTerminalOptions): Promise<ITerminalInstance>;
/**
* 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<IDetachedXtermTerminal>;
/**
* Creates a raw terminal instance, this should not be used outside of the terminal part.
*/
@@ -708,11 +725,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).
*
@@ -947,7 +959,14 @@ export const enum XtermTerminalConstants {
SearchHighlightLimit = 1000
}
export interface IXtermTerminal {
export interface IXtermAttachToElementOptions {
/**
* Whether GPU rendering should be enabled for this element, defaults to true.
*/
enableGpu: boolean;
}
export interface IXtermTerminal extends IDisposable {
/**
* An object that tracks when commands are run and enables navigating and selecting between
* them.
@@ -962,6 +981,11 @@ export interface IXtermTerminal {
readonly onDidChangeSelection: Event<void>;
readonly onDidChangeFindResults: Event<{ resultIndex: number; resultCount: number }>;
/**
* Event fired when focus enters (fires with true) or leaves (false) the terminal.
*/
readonly onDidChangeFocus: Event<boolean>;
/**
* Gets a view of the current texture atlas used by the renderers.
*/
@@ -972,6 +996,18 @@ export interface IXtermTerminal {
*/
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
* @param options Additional options for mounting the terminal in an element
*/
attachToElement(container: HTMLElement, options?: Partial<IXtermAttachToElementOptions>): void;
findResult?: { resultIndex: number; resultCount: number };
/**
@@ -994,6 +1030,34 @@ export interface IXtermTerminal {
*/
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;
@@ -1022,12 +1086,30 @@ export interface IXtermTerminal {
*/
getBufferReverseIterator(): IterableIterator<string>;
/**
* Gets the buffer contents as HTML.
*/
getContentsAsHtml(): Promise<string>;
/**
* Refreshes the terminal after it has been moved.
*/
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.

View File

@@ -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,6 +60,7 @@ 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 { 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 +180,32 @@ export function registerActiveInstanceAction(
});
}
/**
* 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 registerActiveXtermAction(
options: IAction2Options & { run: (activeTerminal: IXtermTerminal, accessor: ServicesAccessor, instance?: ITerminalInstance, args?: unknown) => void | Promise<unknown> }
): IDisposable {
const originalRun = options.run;
return registerTerminalAction({
...options,
run: (c, accessor, args) => {
const activeDetached = Iterable.find(c.service.detachedXterms, d => d.isFocused);
if (activeDetached) {
return originalRun(activeDetached, 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 +596,59 @@ export function registerTerminalActions() {
}
});
registerActiveInstanceAction({
registerActiveXtermAction({
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({
registerActiveXtermAction({
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({
registerActiveXtermAction({
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({
registerActiveXtermAction({
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({
registerActiveXtermAction({
id: TerminalCommandId.ScrollUpPage,
title: { value: localize('workbench.action.terminal.scrollUpPage', "Scroll Up (Page)"), original: 'Scroll Up (Page)' },
f1: true,
@@ -629,38 +656,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({
registerActiveXtermAction({
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({
registerActiveXtermAction({
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 +906,25 @@ export function registerTerminalActions() {
}
});
registerActiveInstanceAction({
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),
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({
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),
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 +1181,7 @@ export function registerTerminalActions() {
}
});
registerActiveInstanceAction({
registerActiveXtermAction({
id: TerminalCommandId.SelectAll,
title: { value: localize('workbench.action.terminal.selectAll', "Select All"), original: 'Select All' },
precondition: ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated),
@@ -1165,9 +1194,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 +1484,48 @@ export function registerTerminalActions() {
// Some commands depend on platform features
if (BrowserFeatures.clipboard.writeText) {
registerActiveInstanceAction({
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?
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({
registerActiveXtermAction({
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({
registerActiveXtermAction({
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)
});
}

View File

@@ -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();
}

View File

@@ -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<typeof XTermTerminal> | undefined;
function getXtermConstructor(keybinding?: ResolvedKeybinding): Promise<typeof XTermTerminal> {
if (xtermConstructor) {
return xtermConstructor;
}
xtermConstructor = Promises.withAsyncBody<typeof XTermTerminal>(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<typeof XTermTerminal>(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<XtermTerminal> {
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');
}
@@ -1072,25 +1060,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance {
async copySelection(asHtml?: boolean, command?: ITerminalCommand): Promise<void> {
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 {
@@ -1101,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));
}

View File

@@ -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, 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';
@@ -47,12 +47,17 @@ 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 { TerminalCapabilityStore } from 'vs/platform/terminal/common/capabilities/terminalCapabilityStore';
export class TerminalService implements ITerminalService {
declare _serviceBrand: undefined;
private _hostActiveTerminals: Map<ITerminalInstanceHost, ITerminalInstance | undefined> = new Map();
private _detachedXterms = new Set<IXtermTerminal>();
private _terminalEditorActive: IContextKey<boolean>;
private readonly _terminalShellTypeContextKey: IContextKey<string>;
@@ -80,6 +85,9 @@ export class TerminalService implements ITerminalService {
get instances(): ITerminalInstance[] {
return this._terminalGroupService.instances.concat(this._terminalEditorService.instances);
}
get detachedXterms(): Iterable<IXtermTerminal> {
return this._detachedXterms;
}
private _reconnectedTerminals: Map<string, ITerminalInstance[]> = new Map();
getReconnectedTerminals(reconnectionOwner: string): ITerminalInstance[] | undefined {
@@ -163,7 +171,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 +980,31 @@ export class TerminalService implements ITerminalService {
return this._createTerminal(shellLaunchConfig, location, options);
}
async createDetachedXterm(options: IDetachedXTermOptions): Promise<IDetachedXtermTerminal> {
const ctor = await TerminalInstance.getXtermConstructor(this._keybindingService, this._contextKeyService);
const instance = this._instantiationService.createInstance(
XtermTerminal,
ctor,
this._configHelper,
options.cols,
options.rows,
options.colorProvider,
options.capabilities || new TerminalCapabilityStore(),
'',
undefined,
false,
);
if (options.readonly) {
instance.raw.attachCustomKeyEventHandler(() => false);
}
this._detachedXterms.add(instance);
instance.onDidDispose(() => this._detachedXterms.delete(instance));
return instance;
}
private async _resolveCwd(shellLaunchConfig: IShellLaunchConfig, splitActiveTerminal: boolean, options?: ICreateTerminalOptions): Promise<void> {
const cwd = shellLaunchConfig.cwd;
if (!cwd) {

View File

@@ -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';
@@ -18,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 } 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';
@@ -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 {
/**
@@ -108,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;
@@ -123,14 +126,14 @@ 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;
private _shellIntegrationAddon: ShellIntegrationAddon;
private _decorationAddon: DecorationAddon;
private _suggestAddon: SuggestAddon;
private _suggestAddon?: SuggestAddon;
// Optional addons
private _canvasAddon?: CanvasAddonType;
@@ -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<boolean>;
private readonly _anyFocusedTerminalHasSelection: IContextKey<boolean>;
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<void>();
readonly onDidChangeSelection = this._onDidChangeSelection.event;
private readonly _onDidChangeFocus = new Emitter<boolean>();
readonly onDidChangeFocus = this._onDidChangeFocus.event;
private readonly _onDidDispose = new Emitter<void>();
readonly onDidDispose = this._onDidDispose.event;
get markTracker(): IMarkTracker { return this._markNavigationAddon; }
get shellIntegration(): IShellIntegration { return this._shellIntegrationAddon; }
@@ -169,6 +179,10 @@ export class XtermTerminal extends DisposableStore implements IXtermTerminal, II
return createImageBitmap(canvas);
}
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
* outside of this class such that {@link raw} is not nullable.
@@ -181,7 +195,7 @@ export class XtermTerminal extends DisposableStore implements IXtermTerminal, II
private readonly _backgroundColorProvider: IXtermColorProvider,
private readonly _capabilities: ITerminalCapabilityStore,
shellIntegrationNonce: string,
private readonly _terminalSuggestWidgetVisibleContextKey: IContextKey<boolean>,
private readonly _terminalSuggestWidgetVisibleContextKey: IContextKey<boolean> | undefined,
disableShellIntegrationReporting: boolean,
@IConfigurationService private readonly _configurationService: IConfigurationService,
@IInstantiationService private readonly _instantiationService: IInstantiationService,
@@ -189,7 +203,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 +258,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,14 +276,29 @@ 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
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<string> {
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<string> {
@@ -286,22 +322,50 @@ export class XtermTerminal extends DisposableStore implements IXtermTerminal, II
return result;
}
attachToElement(container: HTMLElement): HTMLElement {
if (!this._container) {
attachToElement(container: HTMLElement, partialOptions?: Partial<IXtermAttachToElementOptions>): 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 (this._shouldLoadWebgl()) {
this._enableWebglRenderer();
} else if (this._shouldLoadCanvas()) {
this._enableCanvasRenderer();
if (options.enableGpu) {
if (this._shouldLoadWebgl()) {
this._enableWebglRenderer();
} else if (this._shouldLoadCanvas()) {
this._enableCanvasRenderer();
}
}
this._suggestAddon.setContainer(container);
if (!this.raw.element || !this.raw.textarea) {
throw new Error('xterm elements not set after open');
}
this._container = container;
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._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) {
this._onDidChangeFocus.fire(isFocused);
this._anyTerminalFocusContextKey.set(isFocused);
this._anyFocusedTerminalHasSelection.set(isFocused && this.raw.hasSelection());
}
write(data: string | Uint8Array): void {
this.raw.write(data);
}
resize(columns: number, rows: number): void {
this.raw.resize(columns, rows);
}
updateConfig(): void {
@@ -324,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();
@@ -496,6 +562,45 @@ 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.focus();
this.raw.selectAll();
}
focus(): void {
this.raw.focus();
}
async copySelection(asHtml?: boolean, command?: ITerminalCommand): Promise<void> {
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'));
}
}
private _setCursorBlink(blink: boolean): void {
if (this.raw.options.cursorBlink !== blink) {
this.raw.options.cursorBlink = blink;
@@ -765,4 +870,33 @@ 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) {
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 };
}

View File

@@ -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<boolean>(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<boolean>(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<boolean>(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<boolean>(TerminalContextKeyStrings.TextSelected, false, localize('terminalTextSelectedContextKey', "Whether text is selected in the active terminal."));
/** 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<boolean>(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();

View File

@@ -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' } });

View File

@@ -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) {

View File

@@ -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%;
}

View File

@@ -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';
@@ -44,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';
@@ -62,10 +61,17 @@ 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 { IViewDescriptorService, IViewsService } from 'vs/workbench/common/views';
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';
@@ -719,11 +725,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, this.editor, messageContainer)),
this._register(this.instantiationService.createInstance(PlainTextMessagePeek, messageContainer, isInPeekView)),
];
const treeContainer = dom.append(containerElement, dom.$('.test-output-peek-tree'));
@@ -1139,65 +1146,155 @@ class MarkdownTestMessagePeek extends Disposable implements IPeekOutputRenderer
}
}
class PlainTextMessagePeek extends Disposable implements IPeekOutputRenderer {
private readonly widget = this._register(new MutableDisposable<CodeEditorWidget>());
private readonly model = this._register(new MutableDisposable());
private dimension?: dom.IDimension;
private dimensions?: dom.IDimension;
private readonly terminalCwd = this._register(new MutableObservableValue<string>(''));
/** Active terminal instance. */
private readonly terminal = this._register(new MutableDisposable<IDetachedXtermTerminal>());
/** 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,
private readonly isInPeekView: boolean,
@ITestResultService private readonly resultService: ITestResultService,
@ITerminalService private readonly terminalService: ITerminalService,
@IViewDescriptorService private readonly viewDescriptorService: IViewDescriptorService,
@IWorkspaceContextService private readonly workspaceContext: IWorkspaceContextService,
) {
super();
}
private async makeTerminal() {
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;
}
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);
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) {
let uri: URI;
this.outputDataListener.clear();
if (subject instanceof MessageSubject) {
const message = subject.messages[subject.messageIndex];
if (isDiffable(message) || typeof message.message !== 'string') {
return this.clear();
}
uri = subject.messageUri;
this.updateCwd(subject.test.uri);
const terminal = await this.makeTerminal();
terminal.write(message.message);
this.layoutTerminal(terminal);
this.attachTerminalToDom(terminal);
} else {
uri = subject.outputUri;
}
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 }
);
if (this.dimension) {
this.widget.value.layout(this.dimension);
const result = this.resultService.getResult(subject.resultId);
const task = result?.tasks[subject.taskIndex];
if (!task) {
return this.clear();
}
}
this.widget.value.setModel(modelRef.object.textEditorModel);
this.widget.value.updateOptions(commonEditorOptions);
// 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;
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.'));
}
this.attachTerminalToDom(terminal);
this.outputDataListener.value = task.output.onDidWriteData(e => terminal.write(e.buffer));
}
}
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`);
}
private attachTerminalToDom(terminal: IDetachedXtermTerminal) {
terminal.write('\x1b[?25l'); // hide cursor
requestAnimationFrame(() => this.layoutTerminal(terminal));
terminal.attachToElement(this.container, { enableGpu: false });
}
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 layoutTerminal(
xterm: IDetachedXtermTerminal,
width = this.dimensions?.width ?? this.container.clientWidth,
height = this.dimensions?.height ?? this.container.clientHeight
) {
width -= 10 + 20; // scrollbar width + margin
const scaled = getXtermScaledDimensions(xterm.getFont(), width, height);
if (scaled) {
xterm.resize(scaled.cols, scaled.rows);
}
}
}
@@ -1515,6 +1612,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 });
}