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

show outline on command start and rename command icon colors #143404

Merged
merged 4 commits into from Feb 18, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion src/vs/platform/terminal/common/terminal.ts
Expand Up @@ -106,7 +106,7 @@ export const enum TerminalSettingId {
ShellIntegrationShowWelcome = 'terminal.integrated.shellIntegration.showWelcome',
ShellIntegrationCommandIcon = 'terminal.integrated.shellIntegration.commandIcon',
ShellIntegrationCommandIconError = 'terminal.integrated.shellIntegration.commandIconError',
ShellIntegrationCommandIconSkipped = 'terminal.integrated.shellIntegration.commandIconSkipped',
ShellIntegrationCommandIconDefault = 'terminal.integrated.shellIntegration.commandIconDefault',
ShellIntegrationCommandHistory = 'terminal.integrated.shellIntegration.history'
}

Expand Down
Expand Up @@ -8,7 +8,7 @@ import { ILogService } from 'vs/platform/log/common/log';
import { ICommandDetectionCapability, TerminalCapability, ITerminalCommand } from 'vs/workbench/contrib/terminal/common/capabilities/capabilities';
import { IBuffer, IDisposable, IMarker, Terminal } from 'xterm';

interface ICurrentPartialCommand {
export interface ICurrentPartialCommand {
previousCommandMarker?: IMarker;

promptStartMarker?: IMarker;
Expand Down Expand Up @@ -39,6 +39,8 @@ export class CommandDetectionCapability implements ICommandDetectionCapability {

get commands(): readonly ITerminalCommand[] { return this._commands; }

private readonly _onCommandStarted = new Emitter<ITerminalCommand>();
readonly onCommandStarted = this._onCommandStarted.event;
private readonly _onCommandFinished = new Emitter<ITerminalCommand>();
readonly onCommandFinished = this._onCommandFinished.event;

Expand Down Expand Up @@ -70,6 +72,7 @@ export class CommandDetectionCapability implements ICommandDetectionCapability {
handleCommandStart(): void {
this._currentCommand.commandStartX = this._terminal.buffer.active.cursorX;
this._currentCommand.commandStartMarker = this._terminal.registerMarker(0);

// On Windows track all cursor movements after the command start sequence
if (this._isWindowsPty) {
this._commandMarkers.length = 0;
Expand All @@ -82,6 +85,7 @@ export class CommandDetectionCapability implements ICommandDetectionCapability {
}
});
}
this._onCommandStarted.fire({ marker: this._currentCommand.promptStartMarker! } as ITerminalCommand);
this._logService.debug('CommandDetectionCapability#handleCommandStart', this._currentCommand.commandStartX, this._currentCommand.commandStartMarker?.line);
}

Expand Down Expand Up @@ -140,6 +144,7 @@ export class CommandDetectionCapability implements ICommandDetectionCapability {
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 };
Expand Down
4 changes: 2 additions & 2 deletions src/vs/workbench/contrib/terminal/browser/media/terminal.css
Expand Up @@ -423,11 +423,11 @@
top: 50%;
}

.terminal-command-decoration:not(.skipped):hover {
.terminal-command-decoration:not(.default):hover {
cursor: pointer;
border-radius: 5px;
}

.terminal-command-decoration.skipped {
.terminal-command-decoration.default {
pointer-events: none;
}
104 changes: 73 additions & 31 deletions src/vs/workbench/contrib/terminal/browser/xterm/decorationAddon.ts
Expand Up @@ -22,12 +22,12 @@ import { fromNow } from 'vs/base/common/date';
import { toolbarHoverBackground } from 'vs/platform/theme/common/colorRegistry';
import { TerminalSettingId } from 'vs/platform/terminal/common/terminal';
import { editorGutterDeletedBackground, editorGutterModifiedBackground } from 'vs/workbench/contrib/scm/browser/dirtydiffDecorator';
import { TERMINAL_COMMAND_DECORATION_SKIPPED_BACKGROUND_COLOR } from 'vs/workbench/contrib/terminal/common/terminalColorRegistry';
import { TERMINAL_COMMAND_DECORATION_DEFAULT_BACKGROUND_COLOR } from 'vs/workbench/contrib/terminal/common/terminalColorRegistry';

const enum DecorationSelector {
CommandDecoration = 'terminal-command-decoration',
ErrorColor = 'error',
SkippedColor = 'skipped',
DefaultColor = 'default',
Codicon = 'codicon',
XtermScreen = 'xterm-screen'
}
Expand All @@ -37,9 +37,11 @@ interface IDisposableDecoration { decoration: IDecoration; disposables: IDisposa
export class DecorationAddon extends Disposable implements ITerminalAddon {
protected _terminal: Terminal | undefined;
private _hoverDelayer: Delayer<void>;
private _commandListener: IDisposable | undefined;
private _commandStartedListener: IDisposable | undefined;
private _commandFinishedListener: IDisposable | undefined;
private _contextMenuVisible: boolean = false;
private _decorations: Map<number, IDisposableDecoration> = new Map();
private _placeholderDecoration: IDecoration | undefined;

private readonly _onDidRequestRunCommand = this._register(new Emitter<string>());
readonly onDidRequestRunCommand = this._onDidRequestRunCommand.event;
Expand All @@ -54,7 +56,8 @@ export class DecorationAddon extends Disposable implements ITerminalAddon {
super();
this._register({
dispose: () => {
this._commandListener?.dispose();
this._commandStartedListener?.dispose();
this._commandFinishedListener?.dispose();
this._clearDecorations();
}
});
Expand All @@ -74,40 +77,69 @@ export class DecorationAddon extends Disposable implements ITerminalAddon {

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

private _addCommandListener(): void {
if (this._commandListener) {
private _addCommandStartedListener(): void {
if (this._commandStartedListener) {
return;
}
const capability = this._capabilities.get(TerminalCapability.CommandDetection);
if (!capability) {
return;
}
this._commandListener = capability.onCommandFinished(c => this.registerCommandDecoration(c));
this._commandStartedListener = capability.onCommandStarted(command => this.registerCommandDecoration(command));
}


private _addCommandFinishedListener(): void {
if (this._commandFinishedListener) {
return;
}
const capability = this._capabilities.get(TerminalCapability.CommandDetection);
if (!capability) {
return;
}
this._commandFinishedListener = capability.onCommandFinished(command => {
this._placeholderDecoration?.dispose();
this.registerCommandDecoration(command);
});
}

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

registerCommandDecoration(command: ITerminalCommand): IDecoration | undefined {
registerCommandDecoration(command: ITerminalCommand, beforeCommandExecution?: boolean): IDecoration | undefined {
if (!this._terminal) {
return undefined;
}
if (!command.marker) {
throw new Error(`cannot add a decoration for a command ${JSON.stringify(command)} with no marker`);
}
if (!this._terminal) {
return undefined;
if (beforeCommandExecution) {
const decoration = this._terminal.registerDecoration({ marker: command.marker });
if (!decoration) {
return undefined;
}
decoration.onRender(target => {
this._applyStyles(target);
target.classList.add(DecorationSelector.DefaultColor);
});
this._placeholderDecoration = decoration;
return decoration;
}

const decoration = this._terminal.registerDecoration({ marker: command.marker });
Expand All @@ -116,30 +148,34 @@ export class DecorationAddon extends Disposable implements ITerminalAddon {
}

decoration.onRender(target => {
if (decoration.element && !this._decorations.get(decoration.marker.id)) {
const disposables = command.exitCode === undefined ? [] : [this._createContextMenu(decoration.element, command), ...this._createHover(decoration.element, command)];
if (target && !this._decorations.get(decoration.marker.id)) {
const disposables = command.exitCode === undefined ? [] : [this._createContextMenu(target, command), ...this._createHover(target, command)];
this._decorations.set(decoration.marker.id, { decoration, disposables });
}
if (decoration.element?.clientWidth! > 0) {
target.classList.add(DecorationSelector.CommandDecoration);
target.classList.add(DecorationSelector.Codicon);
if (target.clientWidth! > 0) {
this._applyStyles(target);
if (command.exitCode === undefined) {
target.classList.add(DecorationSelector.SkippedColor);
target.classList.add(`codicon-${this._configurationService.getValue(TerminalSettingId.ShellIntegrationCommandIconSkipped)}`);
target.classList.add(DecorationSelector.DefaultColor);
target.classList.add(`codicon-${this._configurationService.getValue(TerminalSettingId.ShellIntegrationCommandIconDefault)}`);
} else if (command.exitCode) {
target.classList.add(DecorationSelector.ErrorColor);
target.classList.add(`codicon-${this._configurationService.getValue(TerminalSettingId.ShellIntegrationCommandIconError)}`);
} else {
target.classList.add(`codicon-${this._configurationService.getValue(TerminalSettingId.ShellIntegrationCommandIcon)}`);
}
// must be inlined to override the inlined styles from xterm
decoration.element!.style.width = '16px';
decoration.element!.style.height = '16px';
}
});
return decoration;
}

private _applyStyles(target: HTMLElement): void {
target.classList.add(DecorationSelector.CommandDecoration);
target.classList.add(DecorationSelector.Codicon);
// must be inlined to override the inlined styles from xterm
target.style.width = '16px';
target.style.height = '16px';
}

private _createContextMenu(target: HTMLElement, command: ITerminalCommand): IDisposable {
// When the xterm Decoration gets disposed of, its element gets removed from the dom
// along with its listeners
Expand Down Expand Up @@ -198,15 +234,21 @@ export class DecorationAddon extends Disposable implements ITerminalAddon {
}

registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => {
const defaultColor = theme.getColor(editorGutterModifiedBackground);
const successColor = theme.getColor(editorGutterModifiedBackground);
const errorColor = theme.getColor(editorGutterDeletedBackground);
const skippedColor = theme.getColor(TERMINAL_COMMAND_DECORATION_SKIPPED_BACKGROUND_COLOR);
const defaultColor = theme.getColor(TERMINAL_COMMAND_DECORATION_DEFAULT_BACKGROUND_COLOR);
const hoverBackgroundColor = theme.getColor(toolbarHoverBackground);
if (!defaultColor || !errorColor || !skippedColor || !hoverBackgroundColor) {
return;

if (successColor) {
collector.addRule(`.${DecorationSelector.CommandDecoration} { color: ${successColor.toString()}; } `);
}
if (errorColor) {
collector.addRule(`.${DecorationSelector.CommandDecoration}.${DecorationSelector.ErrorColor} { color: ${errorColor.toString()}; } `);
}
if (defaultColor) {
collector.addRule(`.${DecorationSelector.CommandDecoration}.${DecorationSelector.DefaultColor} { color: ${defaultColor.toString()};} `);
}
if (hoverBackgroundColor) {
collector.addRule(`.${DecorationSelector.CommandDecoration}:not(.${DecorationSelector.DefaultColor}):hover { background-color: ${hoverBackgroundColor.toString()}; }`);
}
collector.addRule(`.${DecorationSelector.CommandDecoration} { color: ${defaultColor.toString()}; } `);
collector.addRule(`.${DecorationSelector.CommandDecoration}.${DecorationSelector.ErrorColor} { color: ${errorColor.toString()}; } `);
collector.addRule(`.${DecorationSelector.CommandDecoration}.${DecorationSelector.SkippedColor} { color: ${skippedColor.toString()};} `);
collector.addRule(`.${DecorationSelector.CommandDecoration}: not(.${DecorationSelector.SkippedColor}): hover { background-color: ${hoverBackgroundColor.toString()}; }`);
});
Expand Up @@ -78,6 +78,7 @@ export interface ITerminalCapabilityImplMap {
export interface ICommandDetectionCapability {
readonly type: TerminalCapability.CommandDetection;
readonly commands: readonly ITerminalCommand[];
readonly onCommandStarted: Event<ITerminalCommand>;
readonly onCommandFinished: Event<ITerminalCommand>;
setCwd(value: string): void;
setIsWindowsPty(value: boolean): void;
Expand Down
Expand Up @@ -27,11 +27,11 @@ 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_SKIPPED_BACKGROUND_COLOR = registerColor('terminalCommandDecoration.skippedBackground', {
export const TERMINAL_COMMAND_DECORATION_DEFAULT_BACKGROUND_COLOR = registerColor('terminalCommandDecoration.defaultBackground', {
light: '#00000040',
dark: '#ffffff40',
hc: '#ffffff80'
}, nls.localize('terminalCommandDecoration.skippedBackground', 'The terminal command decoration background color when the command was skipped (undefined exit code).'));
}, nls.localize('terminalCommandDecoration.defaultBackground', 'The default terminal command decoration background color.'));
export const TERMINAL_BORDER_COLOR = registerColor('terminal.border', {
dark: PANEL_BORDER,
light: PANEL_BORDER,
Expand Down
Expand Up @@ -106,17 +106,17 @@ const terminalConfiguration: IConfigurationNode = {
[TerminalSettingId.ShellIntegrationCommandIcon]: {
type: 'string',
default: 'primitive-dot',
description: localize('terminal.integrated.shellIntegration.commandIcon', "Controls the icon that will be used for each command in terminals with shell integration enabled that do not have an associated exit code. Set to '' to hide the icon.")
description: localize('terminal.integrated.shellIntegration.commandIconSuccess', "Controls the icon that will be used for each command in terminals with shell integration enabled that do not have an associated exit code. Set to '' to hide the icon.")
},
[TerminalSettingId.ShellIntegrationCommandIconError]: {
type: 'string',
default: 'error-small',
description: localize('terminal.integrated.shellIntegration.commandIconError', "Controls the icon that will be used for each command in terminals with shell integration enabled that do have an associated exit code. Set to '' to hide the icon.")
},
[TerminalSettingId.ShellIntegrationCommandIconSkipped]: {
[TerminalSettingId.ShellIntegrationCommandIconDefault]: {
type: 'string',
default: 'circle-outline',
description: localize('terminal.integrated.shellIntegration.commandIconSkipped', "Controls the icon that will be used for skipped/empty commands. Set to '' to hide the icon.")
description: localize('terminal.integrated.shellIntegration.commandIconDefault', "Controls the icon that will be used for skipped/empty commands. Set to '' to hide the icon.")
},
[TerminalSettingId.TabsFocusMode]: {
type: 'string',
Expand Down