diff --git a/src/vs/platform/terminal/common/terminal.ts b/src/vs/platform/terminal/common/terminal.ts index 6ef19a0e5626c..64ce77f2050d6 100644 --- a/src/vs/platform/terminal/common/terminal.ts +++ b/src/vs/platform/terminal/common/terminal.ts @@ -370,6 +370,13 @@ export interface IHeartbeatService { readonly onBeat: Event; } +export interface TerminalCommand { + command: string; + timestamp: number; + cwd?: string; + exitCode?: number; +} + export interface IShellLaunchConfig { /** * The name of the terminal, if this is not set the name of the process will be used. @@ -548,7 +555,8 @@ export interface IProcessReadyEvent { } export const enum ProcessCapability { - CwdDetection = 'cwdDetection' + CwdDetection = 'cwdDetection', + CommandCognisant = 'commandCognisant' } /** diff --git a/src/vs/platform/terminal/node/ptyService.ts b/src/vs/platform/terminal/node/ptyService.ts index 5768aab671e77..f879374cdb51a 100644 --- a/src/vs/platform/terminal/node/ptyService.ts +++ b/src/vs/platform/terminal/node/ptyService.ts @@ -724,6 +724,7 @@ export class PersistentTerminalProcess extends Disposable { class XtermSerializer implements ITerminalSerializer { private _xterm: XtermTerminal; private _unicodeAddon?: XtermUnicode11Addon; + private _isShellIntegrationEnabled: boolean = false; constructor( cols: number, @@ -735,10 +736,23 @@ class XtermSerializer implements ITerminalSerializer { this._xterm = new XtermTerminal({ cols, rows, scrollback }); if (reviveBuffer) { this._xterm.writeln(reviveBuffer); + if (this._isShellIntegrationEnabled) { + this._xterm.write('\x1b033]133;E\x1b007'); + } } + this._xterm.parser.registerOscHandler(133, (data => this._handleShellIntegration(data))); this.setUnicodeVersion(unicodeVersion); } + private _handleShellIntegration(data: string): boolean { + const [command,] = data.split(';'); + if (command === 'E') { + this._isShellIntegrationEnabled = true; + return true; + } + return false; + } + handleData(data: string): void { this._xterm.write(data); } diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.ts b/src/vs/workbench/contrib/terminal/browser/terminal.ts index a273a9f8551de..a0836949d50ad 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.ts @@ -793,6 +793,12 @@ export interface ITerminalInstance { * clipboard. */ showLinkQuickpick(): Promise; + + /** + * Triggers a quick pick that displays recent commands or cwds. Selecting one will + * re-run it in the active terminal. + */ + runRecent(type: 'command' | 'cwd'): Promise; } export interface IXtermTerminal { @@ -839,6 +845,11 @@ export interface IXtermTerminal { * viewport. */ clearBuffer(): void; + + /* + * When process capabilites are updated, update the command tracker + */ + upgradeCommandTracker(): void; } export interface IRequestAddInstanceToGroupEvent { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts index 99ddb4fba685b..ba03c5b035260 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts @@ -300,6 +300,34 @@ export function registerTerminalActions() { await terminalGroupService.showPanel(true); } }); + registerAction2(class extends Action2 { + constructor() { + super({ + id: TerminalCommandId.RunRecentCommand, + title: { value: localize('workbench.action.terminal.runRecentCommand', "Run Recent Command"), original: 'Run Recent Command' }, + f1: true, + category, + precondition: ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated) + }); + } + async run(accessor: ServicesAccessor): Promise { + await accessor.get(ITerminalService).activeInstance?.runRecent('command'); + } + }); + registerAction2(class extends Action2 { + constructor() { + super({ + id: TerminalCommandId.GoToRecentDirectory, + title: { value: localize('workbench.action.terminal.goToRecentDirectory', "Go to Recent Directory"), original: 'Go to Recent Directory' }, + f1: true, + category, + precondition: ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated) + }); + } + async run(accessor: ServicesAccessor): Promise { + await accessor.get(ITerminalService).activeInstance?.runRecent('cwd'); + } + }); registerAction2(class extends Action2 { constructor() { super({ diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index 837be3290c9be..cd8446d5c2a2f 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -68,6 +68,7 @@ import { escapeNonWindowsPath } from 'vs/platform/terminal/common/terminalEnviro import { IWorkspaceTrustRequestService } from 'vs/platform/workspace/common/workspaceTrust'; import { isFirefox } from 'vs/base/browser/browser'; import { TerminalLinkQuickpick } from 'vs/workbench/contrib/terminal/browser/links/terminalLinkQuickpick'; +import { fromNow } from 'vs/base/common/date'; const enum Constants { /** @@ -697,6 +698,52 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { return { wordLinks: wordResults, webLinks: webResults, fileLinks: fileResults }; } + async runRecent(type: 'command' | 'cwd'): Promise { + const commands = this.xterm?.commandTracker.commands; + if (!commands || !this.xterm) { + return; + } + type Item = IQuickPickItem; + const items: Item[] = []; + if (type === 'command') { + for (const { command, timestamp, cwd, exitCode } of commands) { + // trim off any whitespace and/or line endings + const label = command.trim(); + if (label.length === 0) { + continue; + } + let description = ''; + if (cwd) { + description += `cwd: ${cwd} `; + } + if (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) { + description += 'failed'; + } else { + description += `exitCode: ${exitCode}`; + } + } + items.push({ + label, + description: description.trim(), + detail: fromNow(timestamp, true), + id: timestamp.toString() + }); + } + } else { + const cwds = this.xterm.commandTracker.cwds; + for (const label of cwds) { + items.push({ label }); + } + } + const result = await this._quickInputService.pick(items.reverse(), {}); + if (result) { + this.sendText(type === 'cwd' ? `cd ${result.label}` : result.label, true); + } + } + detachFromElement(): void { this._wrapperElement?.remove(); this._container = undefined; diff --git a/src/vs/workbench/contrib/terminal/browser/xterm/cognisantCommandTrackerAddon.ts b/src/vs/workbench/contrib/terminal/browser/xterm/cognisantCommandTrackerAddon.ts new file mode 100644 index 0000000000000..943672ceaec5f --- /dev/null +++ b/src/vs/workbench/contrib/terminal/browser/xterm/cognisantCommandTrackerAddon.ts @@ -0,0 +1,139 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import type { Terminal, IMarker } from 'xterm'; +import { TerminalCommand } from 'vs/platform/terminal/common/terminal'; +import { Emitter } from 'vs/base/common/event'; +import { CommandTrackerAddon } from 'vs/workbench/contrib/terminal/browser/xterm/commandTrackerAddon'; +import { ILogService } from 'vs/platform/log/common/log'; +import { ShellIntegrationInfo, ShellIntegrationInteraction } from 'vs/workbench/contrib/terminal/browser/xterm/shellIntegrationAddon'; + +interface ICurrentPartialCommand { + marker?: IMarker; + previousCommandMarker?: IMarker; + promptStartY?: number; + commandStartY?: number; + commandStartX?: number; + commandExecutedY?: number; + commandFinishedY?: number; + command?: string; +} + +export class CognisantCommandTrackerAddon extends CommandTrackerAddon { + private _commands: TerminalCommand[] = []; + private _cwds = new Map(); + private _exitCode: number | undefined; + private _cwd: string | undefined; + private _currentCommand: ICurrentPartialCommand = {}; + + protected _terminal: Terminal | undefined; + + private readonly _onCwdChanged = new Emitter(); + readonly onCwdChanged = this._onCwdChanged.event; + + constructor( + @ILogService private readonly _logService: ILogService + ) { + super(); + } + + activate(terminal: Terminal): void { + this._terminal = terminal; + } + + handleIntegratedShellChange(event: { type: string, value: string }): void { + if (!this._terminal) { + return; + } + switch (event.type) { + case ShellIntegrationInfo.CurrentDir: { + this._cwd = event.value; + const freq = this._cwds.get(this._cwd) || 0; + this._cwds.set(this._cwd, freq + 1); + this._onCwdChanged.fire(this._cwd); + break; + } + case ShellIntegrationInteraction.PromptStart: + this._currentCommand.promptStartY = this._terminal.buffer.active.baseY + this._terminal.buffer.active.cursorY; + break; + case ShellIntegrationInteraction.CommandStart: + this._currentCommand.commandStartX = this._terminal.buffer.active.cursorX; + this._currentCommand.commandStartY = this._terminal.buffer.active.baseY + this._terminal.buffer.active.cursorY; + this._currentCommand.marker = this._terminal.registerMarker(0); + break; + case ShellIntegrationInteraction.CommandExecuted: + this._currentCommand.commandExecutedY = this._terminal.buffer.active.baseY + this._terminal.buffer.active.cursorY; + + // TODO: Leverage key events on Windows between CommandStart and Executed to ensure we have the correct line + + // TODO: Only do this on Windows backends + // Check if the command line is the same as the previous command line or if the + // start Y differs from the executed Y. This is to catch the conpty case where the + // "rendering" of the shell integration sequences doesn't occur on the correct cell + // due to https://github.com/microsoft/terminal/issues/11220 + if (this._currentCommand.previousCommandMarker?.line === this._currentCommand.marker?.line || + this._currentCommand.commandStartY === this._currentCommand.commandExecutedY) { + this._currentCommand.marker = this._terminal?.registerMarker(0); + this._currentCommand.commandStartX = 0; + } + + // TODO: This does not yet work when the prompt line is wrapped + this._currentCommand.command = this._terminal!.buffer.active.getLine(this._currentCommand.commandExecutedY)?.translateToString(true, this._currentCommand.commandStartX || 0); + + // TODO: Only do this on Windows backends + // Something went wrong, try predict the prompt based on the shell. + if (this._currentCommand.commandStartX === 0) { + // TODO: Only do this on pwsh + const promptPredictions = [ + `PS ${this._cwd}> `, + `PS>`, + ]; + for (const promptPrediction of promptPredictions) { + if (this._currentCommand.command?.startsWith(promptPrediction)) { + // TODO: Consider cell vs string positioning; test CJK + this._currentCommand.commandStartX = promptPrediction.length; + this._currentCommand.command = this._currentCommand.command.substring(this._currentCommand.commandStartX); + break; + } + } + } + break; + case ShellIntegrationInteraction.CommandFinished: + this._logService.trace('Terminal Command Finished', this._currentCommand.command); + this._exitCode = Number.parseInt(event.value); + if (!this._currentCommand.marker?.line || !this._terminal.buffer.active) { + break; + } + if (this._currentCommand.command && !this._currentCommand.command.startsWith('\\') && this._currentCommand.command !== '') { + this._commands.push({ + command: this._currentCommand.command, + timestamp: Date.now(), + cwd: this._cwd, + exitCode: this._exitCode + }); + } + + this._currentCommand.previousCommandMarker?.dispose(); + this._currentCommand.previousCommandMarker = this._currentCommand.marker; + this._currentCommand.marker = undefined; + break; + default: + return; + } + } + + get commands(): TerminalCommand[] { + return this._commands; + } + + get cwds(): string[] { + const cwds = []; + const sorted = new Map([...this._cwds.entries()].sort((a, b) => b[1] - a[1])); + for (const [key,] of sorted.entries()) { + cwds.push(key); + } + return cwds; + } +} diff --git a/src/vs/workbench/contrib/terminal/browser/xterm/commandTrackerAddon.ts b/src/vs/workbench/contrib/terminal/browser/xterm/commandTrackerAddon.ts index aa08c6581009f..120a7db0d767a 100644 --- a/src/vs/workbench/contrib/terminal/browser/xterm/commandTrackerAddon.ts +++ b/src/vs/workbench/contrib/terminal/browser/xterm/commandTrackerAddon.ts @@ -5,6 +5,7 @@ import type { Terminal, IMarker, ITerminalAddon } from 'xterm'; import { ICommandTracker } from 'vs/workbench/contrib/terminal/common/terminal'; +import { TerminalCommand } from 'vs/platform/terminal/common/terminal'; /** * The minimum size of the prompt in which to assume the line is a command. @@ -21,40 +22,27 @@ export const enum ScrollPosition { Middle } -export class CommandTrackerAddon implements ICommandTracker, ITerminalAddon { +export abstract class CommandTrackerAddon implements ICommandTracker, ITerminalAddon { private _currentMarker: IMarker | Boundary = Boundary.Bottom; private _selectionStart: IMarker | Boundary | null = null; private _isDisposable: boolean = false; - private _terminal: Terminal | undefined; + protected abstract _terminal: Terminal | undefined; - activate(terminal: Terminal): void { - this._terminal = terminal; - terminal.onKey(e => this._onKey(e.key)); - } + abstract get commands(): TerminalCommand[]; + abstract get cwds(): string[]; + abstract activate(terminal: Terminal): void; + abstract handleIntegratedShellChange(event: { type: string, value: string }): void; dispose(): void { } - private _onKey(key: string): void { - if (key === '\x0d') { - this._onEnter(); - } - + clearMarker(): void { // Clear the current marker so successive focus/selection actions are performed from the // bottom of the buffer this._currentMarker = Boundary.Bottom; this._selectionStart = null; } - private _onEnter(): void { - if (!this._terminal) { - return; - } - if (this._terminal.buffer.active.cursorX >= MINIMUM_PROMPT_LENGTH) { - this._terminal.registerMarker(0); - } - } - scrollToPreviousCommand(scrollPosition: ScrollPosition = ScrollPosition.Top, retainSelection: boolean = false): void { if (!this._terminal) { return; @@ -233,13 +221,13 @@ export class CommandTrackerAddon implements ICommandTracker, ITerminalAddon { } if (this._currentMarker === Boundary.Bottom) { - this._currentMarker = this._addMarkerOrThrow(xterm, this._getOffset(xterm) - 1); + this._currentMarker = this._registerMarkerOrThrow(xterm, this._getOffset(xterm) - 1); } else { const offset = this._getOffset(xterm); if (this._isDisposable) { this._currentMarker.dispose(); } - this._currentMarker = this._addMarkerOrThrow(xterm, offset - 1); + this._currentMarker = this._registerMarkerOrThrow(xterm, offset - 1); } this._isDisposable = true; this._scrollToMarker(this._currentMarker, scrollPosition); @@ -256,20 +244,20 @@ export class CommandTrackerAddon implements ICommandTracker, ITerminalAddon { } if (this._currentMarker === Boundary.Top) { - this._currentMarker = this._addMarkerOrThrow(xterm, this._getOffset(xterm) + 1); + this._currentMarker = this._registerMarkerOrThrow(xterm, this._getOffset(xterm) + 1); } else { const offset = this._getOffset(xterm); if (this._isDisposable) { this._currentMarker.dispose(); } - this._currentMarker = this._addMarkerOrThrow(xterm, offset + 1); + this._currentMarker = this._registerMarkerOrThrow(xterm, offset + 1); } this._isDisposable = true; this._scrollToMarker(this._currentMarker, scrollPosition); } - private _addMarkerOrThrow(xterm: Terminal, cursorYOffset: number): IMarker { - const marker = xterm.addMarker(cursorYOffset); + private _registerMarkerOrThrow(xterm: Terminal, cursorYOffset: number): IMarker { + const marker = xterm.registerMarker(cursorYOffset); if (!marker) { throw new Error(`Could not create marker for ${cursorYOffset}`); } @@ -322,3 +310,38 @@ export class CommandTrackerAddon implements ICommandTracker, ITerminalAddon { return xterm.markers.length; } } + +export class NaiveCommandTrackerAddon extends CommandTrackerAddon { + _terminal: Terminal | undefined; + get commands(): TerminalCommand[] { + return []; + } + get cwds(): string[] { + return []; + } + + activate(terminal: Terminal): void { + this._terminal = terminal; + terminal.onKey(e => this._onKey(e.key)); + } + + private _onKey(key: string): void { + if (key === '\x0d') { + this._onEnter(); + } + + this.clearMarker(); + } + + private _onEnter(): void { + if (!this._terminal) { + return; + } + if (this._terminal.buffer.active.cursorX >= MINIMUM_PROMPT_LENGTH) { + this._terminal.registerMarker(0); + } + } + + handleIntegratedShellChange(event: { type: string; value: string; }): void { + } +} diff --git a/src/vs/workbench/contrib/terminal/browser/xterm/shellIntegrationAddon.ts b/src/vs/workbench/contrib/terminal/browser/xterm/shellIntegrationAddon.ts new file mode 100644 index 0000000000000..90bfe638409b4 --- /dev/null +++ b/src/vs/workbench/contrib/terminal/browser/xterm/shellIntegrationAddon.ts @@ -0,0 +1,139 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ITerminalAddon, Terminal } from 'xterm'; +import { IShellIntegration } from 'vs/workbench/contrib/terminal/common/terminal'; +import { Emitter } from 'vs/base/common/event'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { ProcessCapability } from 'vs/platform/terminal/common/terminal'; + +/** + * Shell integration is a feature that enhances the terminal's understanding of what's happening + * in the shell by injecting special sequences into the shell's prompt using the "Set Text + * Parameters" sequence (`OSC Ps ; Pt ST`). + * + * Definitions: + * - OSC: `\x1b]` + * - Ps: A single (usually optional) numeric parameter, composed of one or more digits. + * - Pt: A text parameter composed of printable characters. + * - ST: `\x7` + * + * This is inspired by a feature of the same name in the FinalTerm, iTerm2 and kitty terminals. + */ + +/** + * The identifier for the first numeric parameter (`Ps`) for OSC commands used by shell integration. + */ +const enum ShellIntegrationOscPs { + /** + * Sequences pioneered by FinalTerm. + */ + FinalTerm = 133, + /** + * Sequences pioneered by iTerm. + */ + ITerm = 1337 +} + +/** + * The identifier for the textural parameter (`Pt`) for OSC commands used by shell integration. + */ +const enum ShellIntegrationOscPt { + /** + * The start of the prompt, this is expected to always appear at the start of a line. + */ + PromptStart = 'A', + /** + * The start of a command, ie. where the user inputs their command. + */ + CommandStart = 'B', + /** + * Sent just before the command output begins. + */ + CommandExecuted = 'C', + // TODO: Understand this sequence better and add docs + CommandFinished = 'D', + // TODO: This is a VS Code-specific sequence? Do we need this? Should it have a version? + EnableShellIntegration = 'E', +} + +export const enum ShellIntegrationInfo { + CurrentDir = 'CurrentDir', +} + +export const enum ShellIntegrationInteraction { + PromptStart = 'PROMPT_START', + CommandStart = 'COMMAND_START', + CommandExecuted = 'COMMAND_EXECUTED', + CommandFinished = 'COMMAND_FINISHED' +} + +export class ShellIntegrationAddon extends Disposable implements IShellIntegration, ITerminalAddon { + private _terminal?: Terminal; + + // TODO: Rename ProcessCapability to TerminalCapability, move naive CwdDetection to renderer + private readonly _onCapabilityDisabled = new Emitter(); + readonly onCapabilityDisabled = this._onCapabilityDisabled.event; + private readonly _onCapabilityEnabled = new Emitter(); + readonly onCapabilityEnabled = this._onCapabilityEnabled.event; + private readonly _onIntegratedShellChange = new Emitter<{ type: string, value: string }>(); + readonly onIntegratedShellChange = this._onIntegratedShellChange.event; + + activate(xterm: Terminal) { + this._terminal = xterm; + this._register(xterm.parser.registerOscHandler(ShellIntegrationOscPs.FinalTerm, data => this._handleShellIntegration(data))); + this._register(xterm.parser.registerOscHandler(ShellIntegrationOscPs.ITerm, data => this._updateCwd(data))); + } + + private _handleShellIntegration(data: string): boolean { + if (!this._terminal) { + return false; + } + let type: ShellIntegrationInteraction | undefined; + const [command, exitCode] = data.split(';'); + switch (command) { + case ShellIntegrationOscPt.PromptStart: + type = ShellIntegrationInteraction.PromptStart; + break; + case ShellIntegrationOscPt.CommandStart: + type = ShellIntegrationInteraction.CommandStart; + break; + case ShellIntegrationOscPt.CommandExecuted: + type = ShellIntegrationInteraction.CommandExecuted; + break; + case ShellIntegrationOscPt.CommandFinished: + type = ShellIntegrationInteraction.CommandFinished; + break; + case ShellIntegrationOscPt.EnableShellIntegration: + this._onCapabilityEnabled.fire(ProcessCapability.CommandCognisant); + return true; + default: + return false; + } + const value = exitCode || type; + if (!value) { + return false; + } + this._onIntegratedShellChange.fire({ type, value }); + return true; + } + + private _updateCwd(data: string): boolean { + let value: string | undefined; + const [type, info] = data.split('='); + switch (type) { + case ShellIntegrationInfo.CurrentDir: + value = info; + break; + default: + return false; + } + if (!value) { + return false; + } + this._onIntegratedShellChange.fire({ type, value }); + return true; + } +} diff --git a/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts b/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts index ce036d4174c0c..2268f0f6e45bb 100644 --- a/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts @@ -12,7 +12,7 @@ import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configur import { TerminalConfigHelper } from 'vs/workbench/contrib/terminal/browser/terminalConfigHelper'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; -import { TerminalLocation, TerminalSettingId } from 'vs/platform/terminal/common/terminal'; +import { ProcessCapability, TerminalLocation, TerminalSettingId } from 'vs/platform/terminal/common/terminal'; import { ICommandTracker, ITerminalFont, TERMINAL_VIEW_ID } from 'vs/workbench/contrib/terminal/common/terminal'; import { isSafari } from 'vs/base/browser/browser'; import { IXtermTerminal } from 'vs/workbench/contrib/terminal/browser/terminal'; @@ -20,7 +20,7 @@ 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'; import { INotificationService, IPromptChoice, Severity } from 'vs/platform/notification/common/notification'; -import { CommandTrackerAddon } from 'vs/workbench/contrib/terminal/browser/xterm/commandTrackerAddon'; +import { CommandTrackerAddon, NaiveCommandTrackerAddon } from 'vs/workbench/contrib/terminal/browser/xterm/commandTrackerAddon'; import { localize } from 'vs/nls'; import { IColorTheme, IThemeService } from 'vs/platform/theme/common/themeService'; import { IViewDescriptorService, ViewContainerLocation } from 'vs/workbench/common/views'; @@ -28,6 +28,9 @@ 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 { Color } from 'vs/base/common/color'; +import { ShellIntegrationAddon, ShellIntegrationInteraction } from 'vs/workbench/contrib/terminal/browser/xterm/shellIntegrationAddon'; +import { CognisantCommandTrackerAddon } from 'vs/workbench/contrib/terminal/browser/xterm/cognisantCommandTrackerAddon'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; // How long in milliseconds should an average frame take to render for a notification to appear // which suggests the fallback DOM-based renderer @@ -55,6 +58,7 @@ export class XtermTerminal extends DisposableStore implements IXtermTerminal { // Optional addons private _searchAddon?: SearchAddonType; + private _shellIntegrationAddon?: ShellIntegrationAddon; private _unicode11Addon?: Unicode11AddonType; private _webglAddon?: WebglAddonType; @@ -77,6 +81,7 @@ export class XtermTerminal extends DisposableStore implements IXtermTerminal { rows: number, location: TerminalLocation, @IConfigurationService private readonly _configurationService: IConfigurationService, + @IInstantiationService private readonly _instantiationService: IInstantiationService, @ILogService private readonly _logService: ILogService, @INotificationService private readonly _notificationService: INotificationService, @IStorageService private readonly _storageService: IStorageService, @@ -139,9 +144,37 @@ export class XtermTerminal extends DisposableStore implements IXtermTerminal { // Load addons this._updateUnicodeVersion(); - - this._commandTrackerAddon = new CommandTrackerAddon(); + this._commandTrackerAddon = new NaiveCommandTrackerAddon(); this.raw.loadAddon(this._commandTrackerAddon); + this._shellIntegrationAddon = new ShellIntegrationAddon(); + this.raw.loadAddon(this._shellIntegrationAddon); + + // Hook up co-dependent addon events + this._shellIntegrationAddon.onCapabilityEnabled(e => { + if (e === ProcessCapability.CommandCognisant) { + this.upgradeCommandTracker(); + } + }); + this._shellIntegrationAddon.onIntegratedShellChange(e => { + if (e.type === ShellIntegrationInteraction.CommandFinished) { + // TODO: This shoudl move into the new command tracker + if (this.raw.buffer.active.cursorX >= 2) { + this.raw.registerMarker(0); + this.commandTracker.clearMarker(); + } + } + this._commandTrackerAddon.handleIntegratedShellChange(e); + }); + + } + + upgradeCommandTracker(): void { + if (this._commandTrackerAddon instanceof CognisantCommandTrackerAddon) { + return; + } + this._commandTrackerAddon.dispose(); + this._commandTrackerAddon = this._instantiationService.createInstance(CognisantCommandTrackerAddon); + this._commandTrackerAddon.activate(this.raw); } attachToElement(container: HTMLElement) { diff --git a/src/vs/workbench/contrib/terminal/common/terminal.ts b/src/vs/workbench/contrib/terminal/common/terminal.ts index 5539f748f568b..38c007a2d9d4c 100644 --- a/src/vs/workbench/contrib/terminal/common/terminal.ts +++ b/src/vs/workbench/contrib/terminal/common/terminal.ts @@ -8,7 +8,7 @@ import { Event } from 'vs/base/common/event'; import { IDisposable } from 'vs/base/common/lifecycle'; import { IProcessEnvironment, OperatingSystem } from 'vs/base/common/platform'; import { IExtensionPointDescriptor } from 'vs/workbench/services/extensions/common/extensionsRegistry'; -import { IProcessDataEvent, IProcessReadyEvent, IShellLaunchConfig, ITerminalChildProcess, ITerminalLaunchError, ITerminalProfile, ITerminalProfileObject, ITerminalsLayoutInfo, ITerminalsLayoutInfoById, TerminalIcon, TerminalLocationString, IProcessProperty, TitleEventSource, ProcessPropertyType, IFixedTerminalDimensions, IExtensionTerminalProfile, ICreateContributedTerminalProfileOptions, IProcessPropertyMap, ITerminalEnvironment } from 'vs/platform/terminal/common/terminal'; +import { IProcessDataEvent, IProcessReadyEvent, IShellLaunchConfig, ITerminalChildProcess, ITerminalLaunchError, ITerminalProfile, ITerminalProfileObject, ITerminalsLayoutInfo, ITerminalsLayoutInfoById, TerminalIcon, TerminalLocationString, IProcessProperty, TitleEventSource, ProcessPropertyType, IFixedTerminalDimensions, IExtensionTerminalProfile, ICreateContributedTerminalProfileOptions, IProcessPropertyMap, ITerminalEnvironment, TerminalCommand, ProcessCapability } from 'vs/platform/terminal/common/terminal'; import { IEnvironmentVariableInfo } from 'vs/workbench/contrib/terminal/common/environmentVariable'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { URI } from 'vs/base/common/uri'; @@ -326,6 +326,16 @@ export interface ICommandTracker { selectToNextCommand(): void; selectToPreviousLine(): void; selectToNextLine(): void; + get commands(): TerminalCommand[]; + get cwds(): string[]; + clearMarker(): void; +} + +export interface IShellIntegration { + readonly onCapabilityEnabled: Event; + readonly onCapabilityDisabled: Event; + // TODO: Fire more fine-grained and stronger typed events + readonly onIntegratedShellChange: Event<{ type: string, value: string }>; } export interface INavigationMode { @@ -456,6 +466,8 @@ export const enum TerminalCommandId { ShowWordLinkQuickpick = 'workbench.action.terminal.showWordLinkQuickpick', ShowValidatedLinkQuickpick = 'workbench.action.terminal.showValidatedLinkQuickpick', ShowProtocolLinkQuickpick = 'workbench.action.terminal.showProtocolLinkQuickpick', + RunRecentCommand = 'workbench.action.terminal.runRecentCommand', + GoToRecentDirectory = 'workbench.action.terminal.goToRecentDirectory', CopySelection = 'workbench.action.terminal.copySelection', SelectAll = 'workbench.action.terminal.selectAll', DeleteWordLeft = 'workbench.action.terminal.deleteWordLeft', diff --git a/src/vs/workbench/contrib/terminal/test/browser/terminalCommandTracker.test.ts b/src/vs/workbench/contrib/terminal/test/browser/terminalCommandTracker.test.ts index 27b06adecd6e6..db169b7dc35cb 100644 --- a/src/vs/workbench/contrib/terminal/test/browser/terminalCommandTracker.test.ts +++ b/src/vs/workbench/contrib/terminal/test/browser/terminalCommandTracker.test.ts @@ -5,7 +5,7 @@ import * as assert from 'assert'; import { Terminal } from 'xterm'; -import { CommandTrackerAddon } from 'vs/workbench/contrib/terminal/browser/xterm/commandTrackerAddon'; +import { CommandTrackerAddon, NaiveCommandTrackerAddon } from 'vs/workbench/contrib/terminal/browser/xterm/commandTrackerAddon'; import { isWindows } from 'vs/base/common/platform'; import { IXtermCore } from 'vs/workbench/contrib/terminal/browser/xterm-private'; import { timeout } from 'vs/base/common/async'; @@ -48,7 +48,7 @@ suite('Workbench - TerminalCommandTracker', function () { data += `${i}\n`; } await writeP(xterm, data); - commandTracker = new CommandTrackerAddon(); + commandTracker = new NaiveCommandTrackerAddon(); xterm.loadAddon(commandTracker); });