Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add terminal shell integration decorations #142538

Merged
merged 42 commits into from
Feb 10, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
4437759
add initial decorations code
meganrogge Jan 31, 2022
f8d84d4
Merge branch 'main' into merogge/decorations
meganrogge Jan 31, 2022
f453170
add hover css and text
meganrogge Jan 31, 2022
c53b6a6
pull out into separate method
meganrogge Jan 31, 2022
c20e28d
try
meganrogge Feb 3, 2022
2fd9518
improve
meganrogge Feb 3, 2022
7adaf3a
use css class
meganrogge Feb 3, 2022
7240de4
add css
meganrogge Feb 3, 2022
e8124be
add decorationsAddon
meganrogge Feb 4, 2022
5c09deb
clean up
meganrogge Feb 4, 2022
9da35ad
add error color
meganrogge Feb 7, 2022
c831f8a
Merge branch 'main' into merogge/decorations
meganrogge Feb 7, 2022
0a8b1c9
fix
meganrogge Feb 7, 2022
47fb0c8
dispose of decorations on dispose of command detection capability
meganrogge Feb 7, 2022
0707aa1
more cleanup
meganrogge Feb 7, 2022
47b2e50
get it to work
meganrogge Feb 7, 2022
e1f8e13
add colors from theme
meganrogge Feb 7, 2022
98b13dc
fix color
meganrogge Feb 7, 2022
eb776ee
clear when clear is typed
meganrogge Feb 7, 2022
7991268
fix colors
meganrogge Feb 7, 2022
0eb9a50
always return decoration or throw
meganrogge Feb 7, 2022
a5b4ae7
add context menu
meganrogge Feb 8, 2022
c884b87
get hover and context menu to work
meganrogge Feb 8, 2022
a2efe63
clean up w const enum
meganrogge Feb 8, 2022
5cfd056
more cleanup
meganrogge Feb 8, 2022
1487480
fix some bugs
meganrogge Feb 8, 2022
94218b6
add descriptive hover
meganrogge Feb 8, 2022
4ad3355
clean up
meganrogge Feb 8, 2022
311aad2
more tidying
meganrogge Feb 8, 2022
4637c99
on clear, dispose of decorations
meganrogge Feb 8, 2022
91c697c
improve color registry names
meganrogge Feb 8, 2022
da065fb
add hover delay
meganrogge Feb 8, 2022
c7affbe
fix things
meganrogge Feb 9, 2022
bc5ec07
Merge branch 'main' into merogge/decorations
meganrogge Feb 9, 2022
dd920f0
on render create the context menu, attach the classes, etc
meganrogge Feb 9, 2022
b751d97
use positive integer for styling
meganrogge Feb 9, 2022
69ab19b
Merge branch 'main' into merogge/decorations
meganrogge Feb 10, 2022
2802b1d
fix errors
meganrogge Feb 10, 2022
0ecbd02
clean up
meganrogge Feb 10, 2022
91cfedf
Update src/vs/workbench/contrib/terminal/browser/xterm/decorationAddo…
meganrogge Feb 10, 2022
1412c6d
more cleanup
meganrogge Feb 10, 2022
615587a
don't show button if marker is disposed
meganrogge Feb 10, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,7 @@ export class CommandDetectionCapability implements ICommandDetectionCapability {
constructor(
private readonly _terminal: Terminal,
@ILogService private readonly _logService: ILogService
) {
}
) { }

setCwd(value: string) {
this._cwd = value;
Expand Down Expand Up @@ -131,19 +130,23 @@ export class CommandDetectionCapability implements ICommandDetectionCapability {
const command = this._currentCommand.command;
this._logService.debug('CommandDetectionCapability#handleCommandFinished', this._terminal.buffer.active.cursorX, this._currentCommand.commandFinishedMarker?.line, this._currentCommand.command, this._currentCommand);
this._exitCode = exitCode;

if (this._currentCommand.commandStartMarker === undefined || !this._terminal.buffer.active) {
return;
}
if (command !== undefined && !command.startsWith('\\')) {
const buffer = this._terminal.buffer.active;
const clonedPartialCommand = { ...this._currentCommand };
const timestamp = Date.now();
const newCommand = {
command,
timestamp: Date.now(),
marker: this._currentCommand.commandStartMarker,
endMarker: this._currentCommand.commandFinishedMarker,
timestamp,
cwd: this._cwd,
exitCode: this._exitCode,
getOutput: () => getOutputForCommand(clonedPartialCommand, buffer),
marker: this._currentCommand.commandStartMarker
hasOutput: (this._currentCommand.commandExecutedMarker!.line < this._currentCommand.commandFinishedMarker!.line),
getOutput: () => getOutputForCommand(clonedPartialCommand, buffer)
};
this._commands.push(newCommand);
this._logService.debug('CommandDetectionCapability#onCommandFinished', newCommand);
Expand All @@ -164,6 +167,9 @@ function getOutputForCommand(command: ICurrentPartialCommand, buffer: IBuffer):
const startLine = command.commandExecutedMarker!.line;
const endLine = command.commandFinishedMarker!.line;

if (startLine === endLine) {
return undefined;
}
let output = '';
for (let i = startLine; i < endLine; i++) {
output += buffer.getLine(i)?.translateToString() + '\n';
Expand Down
11 changes: 11 additions & 0 deletions src/vs/workbench/contrib/terminal/browser/media/terminal.css
Original file line number Diff line number Diff line change
Expand Up @@ -412,3 +412,14 @@
.monaco-workbench .pane-body.integrated-terminal .terminal-group > .monaco-split-view2.vertical .terminal-drop-overlay.drop-after {
top: 50%;
}

.terminal-command-decoration {
transition: opacity 0.5s;
transition: all .2s ease-in-out;
margin-left: -1%;
meganrogge marked this conversation as resolved.
Show resolved Hide resolved
width: 50%;
}

.terminal-command-decoration:hover {
transform: scale(2, 1);
}
5 changes: 5 additions & 0 deletions src/vs/workbench/contrib/terminal/browser/media/xterm.css
Original file line number Diff line number Diff line change
Expand Up @@ -177,3 +177,8 @@
.xterm-strikethrough {
text-decoration: line-through;
}

.xterm-screen .xterm-decoration-container .xterm-decoration {
z-index: 6;
position: absolute;
}
25 changes: 13 additions & 12 deletions src/vs/workbench/contrib/terminal/browser/terminalInstance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -637,6 +637,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance {
const lineDataEventAddon = new LineDataEventAddon();
this.xterm.raw.loadAddon(lineDataEventAddon);
this.updateAccessibilitySupport();
this.xterm.onDidRequestRunCommand(command => this.sendText(command, true));
// Write initial text, deferring onLineFeed listener when applicable to avoid firing
// onLineData events containing initialText
if (this._shellLaunchConfig.initialText) {
Expand Down Expand Up @@ -762,23 +763,23 @@ export class TerminalInstance extends Disposable implements ITerminalInstance {
type Item = IQuickPickItem & { command?: ITerminalCommand };
const items: Item[] = [];
if (type === 'command') {
for (const { command, timestamp, cwd, exitCode, getOutput } of commands) {
for (const entry of commands) {
// trim off any whitespace and/or line endings
const label = command.trim();
const label = entry.command.trim();
if (label.length === 0) {
continue;
}
let detail = '';
if (cwd) {
detail += `cwd: ${cwd} `;
if (entry.cwd) {
detail += `cwd: ${entry.cwd} `;
}
if (exitCode) {
if (entry.exitCode) {
// Since you cannot get the last command's exit code on pwsh, just whether it failed
// or not, -1 is treated specially as simply failed
if (exitCode === -1) {
if (entry.exitCode === -1) {
detail += 'failed';
} else {
detail += `exitCode: ${exitCode}`;
detail += `exitCode: ${entry.exitCode}`;
}
}
detail = detail.trim();
Expand All @@ -790,17 +791,17 @@ export class TerminalInstance extends Disposable implements ITerminalInstance {
}];
// Merge consecutive commands
if (items.length > 0 && items[items.length - 1].label === label) {
items[items.length - 1].id = timestamp.toString();
items[items.length - 1].id = entry.timestamp.toString();
items[items.length - 1].detail = detail;
continue;
}
items.push({
label,
description: fromNow(timestamp, true),
description: fromNow(entry.timestamp, true),
detail,
id: timestamp.toString(),
command: { command, timestamp, cwd, exitCode, getOutput },
buttons
id: entry.timestamp.toString(),
command: entry,
buttons: (!entry.endMarker?.isDisposed && !entry.marker?.isDisposed && (entry.endMarker!.line - entry.marker!.line > 0)) ? buttons : undefined
});
}
} else {
Expand Down
177 changes: 177 additions & 0 deletions src/vs/workbench/contrib/terminal/browser/xterm/decorationAddon.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle';
import { ITerminalCommand } from 'vs/workbench/contrib/terminal/common/terminal';
import { IDecoration, ITerminalAddon, Terminal } from 'xterm';
import * as dom from 'vs/base/browser/dom';
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
import { ITerminalCapabilityStore, TerminalCapability } from 'vs/workbench/contrib/terminal/common/capabilities/capabilities';
import { IColorTheme, ICssStyleCollector, registerThemingParticipant } from 'vs/platform/theme/common/themeService';
import { TERMINAL_COMMAND_DECORATION_DEFAULT_BACKGROUND_COLOR, TERMINAL_COMMAND_DECORATION_ERROR_BACKGROUND_COLOR } from 'vs/workbench/contrib/terminal/common/terminalColorRegistry';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { IHoverService } from 'vs/workbench/services/hover/browser/hover';
import { IAction } from 'vs/base/common/actions';
import { Emitter } from 'vs/base/common/event';
import { MarkdownString } from 'vs/base/common/htmlContent';
import { localize } from 'vs/nls';
import { Delayer } from 'vs/base/common/async';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { fromNow } from 'vs/base/common/date';
import { isWindows } from 'vs/base/common/platform';

const enum DecorationSelector {
CommandDecoration = 'terminal-command-decoration',
Error = 'error',
}

export class DecorationAddon extends Disposable implements ITerminalAddon {
private _decorations: IDecoration[] = [];
protected _terminal: Terminal | undefined;
private _hoverDelayer: Delayer<void>;
private _commandListener: IDisposable | undefined;

private readonly _onDidRequestRunCommand = this._register(new Emitter<string>());
readonly onDidRequestRunCommand = this._onDidRequestRunCommand.event;

constructor(
private readonly _capabilities: ITerminalCapabilityStore,
@IClipboardService private readonly _clipboardService: IClipboardService,
@IContextMenuService private readonly _contextMenuService: IContextMenuService,
@IHoverService private readonly _hoverService: IHoverService,
@IConfigurationService private readonly _configurationService: IConfigurationService,
) {
super();
this._register({
dispose: () => {
dispose(this._decorations);
this._commandListener?.dispose();
}
});
this._attachToCommandCapability();
this._hoverDelayer = this._register(new Delayer(this._configurationService.getValue('workbench.hover.delay')));
}

private _attachToCommandCapability(): void {
if (this._capabilities.has(TerminalCapability.CommandDetection)) {
this._addCommandListener();
} else {
this._register(this._capabilities.onDidAddCapability(c => {
if (c === TerminalCapability.CommandDetection) {
this._addCommandListener();
}
}));
}
this._register(this._capabilities.onDidRemoveCapability(c => {
if (c === TerminalCapability.CommandDetection) {
this._commandListener?.dispose();
}
}));
}

private _addCommandListener(): void {
if (this._commandListener) {
return;
}
const capability = this._capabilities.get(TerminalCapability.CommandDetection);
if (!capability) {
return;
}
this._commandListener = capability.onCommandFinished(c => {
//TODO: remove when this has been fixed in xterm.js
if (!isWindows && c.command === 'clear') {
this._terminal?.clear();
dispose(this._decorations);
return;
}
const element = this.registerCommandDecoration(c);
if (element) {
this._decorations.push(element);
}
});
}

activate(terminal: Terminal): void {
this._terminal = terminal;
}

registerCommandDecoration(command: ITerminalCommand): IDecoration | undefined {
if (!command.marker) {
throw new Error(`cannot add decoration for command: ${command}, and terminal: ${this._terminal}`);
}
if (!this._terminal || command.command.trim().length === 0) {
return undefined;
}

const decoration = this._terminal.registerDecoration({ marker: command.marker });

decoration?.onRender(target => {
this._createContextMenu(target, command);
this._createHover(target, command);

target.classList.add(DecorationSelector.CommandDecoration);
if (command.exitCode) {
target.classList.add(DecorationSelector.Error);
}
});

return decoration;
}

private _createContextMenu(target: HTMLElement, command: ITerminalCommand) {
// When the xterm Decoration gets disposed of, its element gets removed from the dom
// along with its listeners
dom.addDisposableListener(target, dom.EventType.CLICK, async () => {
const actions = await this._getCommandActions(command);
this._contextMenuService.showContextMenu({ getAnchor: () => target, getActions: () => actions });
});
}

private _createHover(target: HTMLElement, command: ITerminalCommand): void {
// When the xterm Decoration gets disposed of, its element gets removed from the dom
// along with its listeners
dom.addDisposableListener(target, dom.EventType.MOUSE_ENTER, async () => {
let hoverContent = `${localize('terminal-prompt-context-menu', "Show Actions")}` + ` ...${fromNow(command.timestamp)} `;
if (command.exitCode) {
hoverContent += `\n\n\n\nExit Code: ${command.exitCode} `;
}
const hoverOptions = { content: new MarkdownString(hoverContent), target };
await this._hoverDelayer.trigger(() => {
this._hoverService.showHover(hoverOptions);
});
});
dom.addDisposableListener(target, dom.EventType.MOUSE_LEAVE, async () => {
this._hoverService.hideHover();
});
dom.addDisposableListener(target, dom.EventType.MOUSE_OUT, async () => {
this._hoverService.hideHover();
});
dom.addDisposableListener(target.parentElement?.parentElement!, 'click', async () => {
this._hoverService.hideHover();
});
}

private async _getCommandActions(command: ITerminalCommand): Promise<IAction[]> {
const actions: IAction[] = [];
if (command.hasOutput) {
actions.push({
class: 'copy-output', tooltip: 'Copy Output', dispose: () => { }, id: 'terminal.copyOutput', label: localize("terminal.copyOutput", 'Copy Output'), enabled: true,
run: () => this._clipboardService.writeText(command.getOutput()!)
});
}
actions.push({
class: 'rerun-command', tooltip: 'Rerun Command', dispose: () => { }, id: 'terminal.rerunCommand', label: localize("terminal.rerunCommand", 'Re-run Command'), enabled: true,
run: () => this._onDidRequestRunCommand.fire(command.command)
});
return actions;
}
}

registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => {
const commandDecorationSuccessColor = theme.getColor(TERMINAL_COMMAND_DECORATION_DEFAULT_BACKGROUND_COLOR);
collector.addRule(`.${DecorationSelector.CommandDecoration} { background-color: ${commandDecorationSuccessColor ? commandDecorationSuccessColor.toString() : ''}; }`);
const commandDecorationErrorColor = theme.getColor(TERMINAL_COMMAND_DECORATION_ERROR_BACKGROUND_COLOR);
collector.addRule(`.${DecorationSelector.CommandDecoration}.${DecorationSelector.Error} { background-color: ${commandDecorationErrorColor ? commandDecorationErrorColor.toString() : ''}; }`);
});
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,13 @@ import { IColorTheme, IThemeService } from 'vs/platform/theme/common/themeServic
import { IViewDescriptorService, ViewContainerLocation } from 'vs/workbench/common/views';
import { editorBackground } from 'vs/platform/theme/common/colorRegistry';
import { PANEL_BACKGROUND, SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme';
import { TERMINAL_FOREGROUND_COLOR, TERMINAL_BACKGROUND_COLOR, TERMINAL_CURSOR_FOREGROUND_COLOR, TERMINAL_CURSOR_BACKGROUND_COLOR, TERMINAL_SELECTION_BACKGROUND_COLOR, ansiColorIdentifiers } from 'vs/workbench/contrib/terminal/common/terminalColorRegistry';
import { TERMINAL_FOREGROUND_COLOR, TERMINAL_BACKGROUND_COLOR, TERMINAL_CURSOR_FOREGROUND_COLOR, TERMINAL_CURSOR_BACKGROUND_COLOR, ansiColorIdentifiers, TERMINAL_SELECTION_BACKGROUND_COLOR } from 'vs/workbench/contrib/terminal/common/terminalColorRegistry';
import { Color } from 'vs/base/common/color';
import { ShellIntegrationAddon } from 'vs/workbench/contrib/terminal/browser/xterm/shellIntegrationAddon';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { DecorationAddon } from 'vs/workbench/contrib/terminal/browser/xterm/decorationAddon';
import { ITerminalCapabilityStore } from 'vs/workbench/contrib/terminal/common/capabilities/capabilities';
import { Emitter } from 'vs/base/common/event';

// How long in milliseconds should an average frame take to render for a notification to appear
// which suggests the fallback DOM-based renderer
Expand Down Expand Up @@ -62,6 +64,9 @@ export class XtermTerminal extends DisposableStore implements IXtermTerminal {
private _unicode11Addon?: Unicode11AddonType;
private _webglAddon?: WebglAddonType;

private readonly _onDidRequestRunCommand = new Emitter<string>();
readonly onDidRequestRunCommand = this._onDidRequestRunCommand.event;

get commandTracker(): ICommandTracker { return this._commandTrackerAddon; }
get shellIntegration(): IShellIntegration { return this._shellIntegrationAddon; }

Expand Down Expand Up @@ -150,6 +155,9 @@ export class XtermTerminal extends DisposableStore implements IXtermTerminal {
this.raw.loadAddon(this._commandTrackerAddon);
this._shellIntegrationAddon = this._instantiationService.createInstance(ShellIntegrationAddon);
this.raw.loadAddon(this._shellIntegrationAddon);
const decorationAddon = this._instantiationService.createInstance(DecorationAddon, capabilities);
decorationAddon.onDidRequestRunCommand(command => this._onDidRequestRunCommand.fire(command));
this.raw.loadAddon(decorationAddon);
}

attachToElement(container: HTMLElement) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,9 @@ export interface ITerminalCommand {
cwd?: string;
exitCode?: number;
marker?: IXtermMarker;
endMarker?: IXtermMarker;
getOutput(): string | undefined;
hasOutput: boolean;
}

/**
Expand Down
12 changes: 11 additions & 1 deletion src/vs/workbench/contrib/terminal/common/terminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'
import { URI } from 'vs/base/common/uri';
import { IProcessDetails } from 'vs/platform/terminal/common/terminalProcess';
import { Registry } from 'vs/platform/registry/common/platform';
import { ITerminalCapabilityStore } from 'vs/workbench/contrib/terminal/common/capabilities/capabilities';
import { ITerminalCapabilityStore, IXtermMarker } from 'vs/workbench/contrib/terminal/common/capabilities/capabilities';

export const TERMINAL_VIEW_ID = 'terminal';

Expand Down Expand Up @@ -330,6 +330,16 @@ export interface IShellIntegration {
capabilities: ITerminalCapabilityStore;
}

export interface ITerminalCommand {
command: string;
timestamp: number;
cwd?: string;
exitCode?: number;
marker?: IXtermMarker;
hasOutput: boolean;
getOutput(): string | undefined;
}

export interface INavigationMode {
exitNavigationMode(): void;
focusPreviousLine(): void;
Expand Down
10 changes: 10 additions & 0 deletions src/vs/workbench/contrib/terminal/common/terminalColorRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,16 @@ export const TERMINAL_SELECTION_BACKGROUND_COLOR = registerColor('terminal.selec
dark: '#FFFFFF40',
hc: '#FFFFFF80'
}, nls.localize('terminal.selectionBackground', 'The selection background color of the terminal.'));
export const TERMINAL_COMMAND_DECORATION_DEFAULT_BACKGROUND_COLOR = registerColor('terminalCommandDecoration.defaultBackground', {
light: '#66afe0',
dark: '#399ee6',
hc: '#399ee6'
}, nls.localize('terminalCommandDecoration.defaultBackground', 'The default terminal command decoration background color.'));
export const TERMINAL_COMMAND_DECORATION_ERROR_BACKGROUND_COLOR = registerColor('terminalCommandDecoration.errorBackground', {
light: '#a1260d',
dark: '#be1100',
hc: '#be1100'
}, nls.localize('terminalCommandDecoration.errorBackground', 'The terminal command decoration background color when there is an exit code.'));
export const TERMINAL_BORDER_COLOR = registerColor('terminal.border', {
dark: PANEL_BORDER,
light: PANEL_BORDER,
Expand Down