diff --git a/src/typings/vscode-xterm.d.ts b/src/typings/vscode-xterm.d.ts index e1127d8428fd3ec5947010ee5841a83a018b67d5..2bbcfac4f8f1fafc92cc3df5ed3841cb8d2716aa 100644 --- a/src/typings/vscode-xterm.d.ts +++ b/src/typings/vscode-xterm.d.ts @@ -619,10 +619,10 @@ declare module 'vscode-xterm' { declare module 'vscode-xterm' { interface Terminal { buffer: { - /** - * The viewport position. - */ + y: number; + ybase: number; ydisp: number; + x: number; }; /** diff --git a/src/vs/workbench/parts/terminal/common/terminal.ts b/src/vs/workbench/parts/terminal/common/terminal.ts index d0496bbcc09056f0f2a3be10fdfc2fdee90d6706..62dc8f0a702d51fb44f16853e485b2a516052ab8 100644 --- a/src/vs/workbench/parts/terminal/common/terminal.ts +++ b/src/vs/workbench/parts/terminal/common/terminal.ts @@ -267,6 +267,12 @@ export interface ITerminalInstance { */ disableLayout: boolean; + /** + * An object that tracks when commands are run and enables navigating and selecting between + * them. + */ + readonly commandTracker: ITerminalCommandTracker; + /** * Dispose the terminal instance, removing it from the panel/service and freeing up resources. */ @@ -440,3 +446,10 @@ export interface ITerminalInstance { addDisposable(disposable: IDisposable): void; } + +export interface ITerminalCommandTracker { + focusPreviousCommand(): void; + focusNextCommand(): void; + selectToPreviousCommand(): void; + selectToNextCommand(): void; +} \ No newline at end of file diff --git a/src/vs/workbench/parts/terminal/electron-browser/terminal.contribution.ts b/src/vs/workbench/parts/terminal/electron-browser/terminal.contribution.ts index 9456504e8c21fcc080a214aa2a9071650a7a6322..036f94033e1c760fa099ee09ade67c42cc55f63f 100644 --- a/src/vs/workbench/parts/terminal/electron-browser/terminal.contribution.ts +++ b/src/vs/workbench/parts/terminal/electron-browser/terminal.contribution.ts @@ -18,7 +18,7 @@ import { getTerminalDefaultShellUnixLike, getTerminalDefaultShellWindows } from import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/workbench/common/actions'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import { KillTerminalAction, CopyTerminalSelectionAction, CreateNewTerminalAction, CreateNewInActiveWorkspaceTerminalAction, FocusActiveTerminalAction, FocusNextTerminalAction, FocusPreviousTerminalAction, SelectDefaultShellWindowsTerminalAction, RunSelectedTextInTerminalAction, RunActiveFileInTerminalAction, ScrollDownTerminalAction, ScrollDownPageTerminalAction, ScrollToBottomTerminalAction, ScrollUpTerminalAction, ScrollUpPageTerminalAction, ScrollToTopTerminalAction, TerminalPasteAction, ToggleTerminalAction, ClearTerminalAction, AllowWorkspaceShellTerminalCommand, DisallowWorkspaceShellTerminalCommand, RenameTerminalAction, SelectAllTerminalAction, FocusTerminalFindWidgetAction, HideTerminalFindWidgetAction, ShowNextFindTermTerminalFindWidgetAction, ShowPreviousFindTermTerminalFindWidgetAction, DeleteWordLeftTerminalAction, DeleteWordRightTerminalAction, QuickOpenActionTermContributor, QuickOpenTermAction, TERMINAL_PICKER_PREFIX, MoveToLineStartTerminalAction, MoveToLineEndTerminalAction, SplitTerminalAction, FocusPreviousPaneTerminalAction, FocusNextPaneTerminalAction, ResizePaneLeftTerminalAction, ResizePaneRightTerminalAction, ResizePaneUpTerminalAction, ResizePaneDownTerminalAction } from 'vs/workbench/parts/terminal/electron-browser/terminalActions'; +import { KillTerminalAction, CopyTerminalSelectionAction, CreateNewTerminalAction, CreateNewInActiveWorkspaceTerminalAction, FocusActiveTerminalAction, FocusNextTerminalAction, FocusPreviousTerminalAction, SelectDefaultShellWindowsTerminalAction, RunSelectedTextInTerminalAction, RunActiveFileInTerminalAction, ScrollDownTerminalAction, ScrollDownPageTerminalAction, ScrollToBottomTerminalAction, ScrollUpTerminalAction, ScrollUpPageTerminalAction, ScrollToTopTerminalAction, TerminalPasteAction, ToggleTerminalAction, ClearTerminalAction, AllowWorkspaceShellTerminalCommand, DisallowWorkspaceShellTerminalCommand, RenameTerminalAction, SelectAllTerminalAction, FocusTerminalFindWidgetAction, HideTerminalFindWidgetAction, ShowNextFindTermTerminalFindWidgetAction, ShowPreviousFindTermTerminalFindWidgetAction, DeleteWordLeftTerminalAction, DeleteWordRightTerminalAction, QuickOpenActionTermContributor, QuickOpenTermAction, TERMINAL_PICKER_PREFIX, MoveToLineStartTerminalAction, MoveToLineEndTerminalAction, SplitTerminalAction, FocusPreviousPaneTerminalAction, FocusNextPaneTerminalAction, ResizePaneLeftTerminalAction, ResizePaneRightTerminalAction, ResizePaneUpTerminalAction, ResizePaneDownTerminalAction, FocusPreviousCommandAction, FocusNextCommandAction, SelectToPreviousCommandAction, SelectToNextCommandAction } from 'vs/workbench/parts/terminal/electron-browser/terminalActions'; import { Registry } from 'vs/platform/registry/common/platform'; import { ShowAllCommandsAction } from 'vs/workbench/parts/quickopen/browser/commandsHandler'; import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; @@ -279,7 +279,11 @@ configurationRegistry.registerConfiguration({ ResizePaneLeftTerminalAction.ID, ResizePaneRightTerminalAction.ID, ResizePaneUpTerminalAction.ID, - ResizePaneDownTerminalAction.ID + ResizePaneDownTerminalAction.ID, + FocusPreviousCommandAction.ID, + FocusNextCommandAction.ID, + SelectToPreviousCommandAction.ID, + SelectToNextCommandAction.ID ].sort() }, 'terminal.integrated.env.osx': { @@ -474,6 +478,22 @@ actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(ResizePaneDownTe linux: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.DownArrow }, mac: { primary: KeyMod.CtrlCmd | KeyMod.WinCtrl | KeyCode.DownArrow } }, KEYBINDING_CONTEXT_TERMINAL_FOCUS), 'Terminal: Resize Pane Down', category); +actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(FocusPreviousCommandAction, FocusPreviousCommandAction.ID, FocusPreviousCommandAction.LABEL, { + primary: null, + mac: { primary: KeyMod.CtrlCmd | KeyCode.UpArrow } +}, KEYBINDING_CONTEXT_TERMINAL_FOCUS), 'Terminal: Focus Previous Command', category); +actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(FocusNextCommandAction, FocusNextCommandAction.ID, FocusNextCommandAction.LABEL, { + primary: null, + mac: { primary: KeyMod.CtrlCmd | KeyCode.DownArrow } +}, KEYBINDING_CONTEXT_TERMINAL_FOCUS), 'Terminal: Focus Next Command', category); +actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(SelectToPreviousCommandAction, SelectToPreviousCommandAction.ID, SelectToPreviousCommandAction.LABEL, { + primary: null, + mac: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.UpArrow } +}, KEYBINDING_CONTEXT_TERMINAL_FOCUS), 'Terminal: Select To Previous Command', category); +actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(SelectToNextCommandAction, SelectToNextCommandAction.ID, SelectToNextCommandAction.LABEL, { + primary: null, + mac: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.DownArrow } +}, KEYBINDING_CONTEXT_TERMINAL_FOCUS), 'Terminal: Select To Next Command', category); terminalCommands.setup(); diff --git a/src/vs/workbench/parts/terminal/electron-browser/terminalActions.ts b/src/vs/workbench/parts/terminal/electron-browser/terminalActions.ts index 7002bbcbad1a9114b270802dbffb1fe2b6f00929..b5424e23f96a5eb0a97f242f2f78fb0c9750673f 100644 --- a/src/vs/workbench/parts/terminal/electron-browser/terminalActions.ts +++ b/src/vs/workbench/parts/terminal/electron-browser/terminalActions.ts @@ -977,3 +977,83 @@ export class RenameTerminalQuickOpenAction extends RenameTerminalAction { return TPromise.as(null); } } + +export class FocusPreviousCommandAction extends Action { + public static readonly ID = 'workbench.action.terminal.focusPreviousCommand'; + public static readonly LABEL = nls.localize('workbench.action.terminal.focusPreviousCommand', "Focus Previous Command"); + + constructor( + id: string, label: string, + @ITerminalService private terminalService: ITerminalService + ) { + super(id, label); + } + + public run(): TPromise { + const instance = this.terminalService.getActiveInstance(); + if (instance) { + instance.commandTracker.focusPreviousCommand(); + } + return TPromise.as(void 0); + } +} + +export class FocusNextCommandAction extends Action { + public static readonly ID = 'workbench.action.terminal.focusNextCommand'; + public static readonly LABEL = nls.localize('workbench.action.terminal.focusNextCommand', "Focus Next Command"); + + constructor( + id: string, label: string, + @ITerminalService private terminalService: ITerminalService + ) { + super(id, label); + } + + public run(): TPromise { + const instance = this.terminalService.getActiveInstance(); + if (instance) { + instance.commandTracker.focusNextCommand(); + } + return TPromise.as(void 0); + } +} + +export class SelectToPreviousCommandAction extends Action { + public static readonly ID = 'workbench.action.terminal.selectToPreviousCommand'; + public static readonly LABEL = nls.localize('workbench.action.terminal.selectToPreviousCommand', "Select To Previous Command"); + + constructor( + id: string, label: string, + @ITerminalService private terminalService: ITerminalService + ) { + super(id, label); + } + + public run(): TPromise { + const instance = this.terminalService.getActiveInstance(); + if (instance) { + instance.commandTracker.selectToPreviousCommand(); + } + return TPromise.as(void 0); + } +} + +export class SelectToNextCommandAction extends Action { + public static readonly ID = 'workbench.action.terminal.selectToNextCommand'; + public static readonly LABEL = nls.localize('workbench.action.terminal.selectToNextCommand', "Select To Next Command"); + + constructor( + id: string, label: string, + @ITerminalService private terminalService: ITerminalService + ) { + super(id, label); + } + + public run(): TPromise { + const instance = this.terminalService.getActiveInstance(); + if (instance) { + instance.commandTracker.selectToNextCommand(); + } + return TPromise.as(void 0); + } +} diff --git a/src/vs/workbench/parts/terminal/electron-browser/terminalInstance.ts b/src/vs/workbench/parts/terminal/electron-browser/terminalInstance.ts index 0419980bf2fbbcf99b9094304d23f411253e1659..21ec8a5ea726a8f88a54839c343076ca0d759dff 100644 --- a/src/vs/workbench/parts/terminal/electron-browser/terminalInstance.ts +++ b/src/vs/workbench/parts/terminal/electron-browser/terminalInstance.ts @@ -40,6 +40,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { ILogService } from 'vs/platform/log/common/log'; +import { TerminalCommandTracker } from 'vs/workbench/parts/terminal/node/terminalCommandTracker'; /** The amount of time to consider terminal errors to be related to the launch */ const LAUNCHING_DURATION = 500; @@ -103,6 +104,7 @@ export class TerminalInstance implements ITerminalInstance { private _widgetManager: TerminalWidgetManager; private _linkHandler: TerminalLinkHandler; + private _commandTracker: TerminalCommandTracker; public disableLayout: boolean; public get id(): number { return this._id; } @@ -115,6 +117,7 @@ export class TerminalInstance implements ITerminalInstance { public get hadFocusOnExit(): boolean { return this._hadFocusOnExit; } public get isTitleSetByProcess(): boolean { return !!this._messageTitleListener; } public get shellLaunchConfig(): IShellLaunchConfig { return Object.freeze(this._shellLaunchConfig); } + public get commandTracker(): TerminalCommandTracker { return this._commandTracker; } public constructor( private _terminalFocusContextKey: IContextKey, @@ -330,6 +333,7 @@ export class TerminalInstance implements ITerminalInstance { return false; }); this._linkHandler = this._instantiationService.createInstance(TerminalLinkHandler, this._xterm, platform.platform, this._initialCwd); + this._commandTracker = new TerminalCommandTracker(this._xterm); this._instanceDisposables.push(this._themeService.onThemeChange(theme => this._updateTheme(theme))); } diff --git a/src/vs/workbench/parts/terminal/node/terminalCommandTracker.ts b/src/vs/workbench/parts/terminal/node/terminalCommandTracker.ts new file mode 100644 index 0000000000000000000000000000000000000000..f7c06906327c9c215635fc8757f1f35dddd302d5 --- /dev/null +++ b/src/vs/workbench/parts/terminal/node/terminalCommandTracker.ts @@ -0,0 +1,144 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Terminal, IMarker } from 'vscode-xterm'; +import { ITerminalCommandTracker } from 'vs/workbench/parts/terminal/common/terminal'; + +/** + * The minimize size of the prompt in which to assume the line is a command. + */ +const MINIMUM_PROMPT_LENGTH = 2; + +enum Boundary { + Top, + Bottom +} + +export class TerminalCommandTracker implements ITerminalCommandTracker { + private _currentMarker: IMarker | Boundary = Boundary.Bottom; + private _selectionStart: IMarker | Boundary | null = null; + + constructor( + private _xterm: Terminal + ) { + this._xterm.on('key', key => this._onKey(key)); + } + + private _onKey(key: string): void { + if (key === '\x0d') { + this._onEnter(); + } + + // 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._xterm.buffer.x >= MINIMUM_PROMPT_LENGTH) { + this._xterm.addMarker(0); + } + } + + public focusPreviousCommand(retainSelection: boolean = false): void { + if (!retainSelection) { + this._selectionStart = null; + } + + let markerIndex; + if (this._currentMarker === Boundary.Bottom) { + markerIndex = this._xterm.markers.length - 1; + } else if (this._currentMarker === Boundary.Top) { + markerIndex = -1; + } else { + markerIndex = this._xterm.markers.indexOf(this._currentMarker) - 1; + } + + if (markerIndex < 0) { + this._currentMarker = Boundary.Top; + this._xterm.scrollToTop(); + return; + } + + this._currentMarker = this._xterm.markers[markerIndex]; + this._xterm.scrollToLine(this._currentMarker.line); + } + + public focusNextCommand(retainSelection: boolean = false): void { + if (!retainSelection) { + this._selectionStart = null; + } + + let markerIndex; + if (this._currentMarker === Boundary.Bottom) { + markerIndex = this._xterm.markers.length; + } else if (this._currentMarker === Boundary.Top) { + markerIndex = 0; + } else { + markerIndex = this._xterm.markers.indexOf(this._currentMarker) + 1; + } + + if (markerIndex >= this._xterm.markers.length) { + this._currentMarker = Boundary.Bottom; + this._xterm.scrollToBottom(); + return; + } + + this._currentMarker = this._xterm.markers[markerIndex]; + this._xterm.scrollToLine(this._currentMarker.line); + } + + public selectToPreviousCommand(): void { + if (this._selectionStart === null) { + this._selectionStart = this._currentMarker; + } + this.focusPreviousCommand(true); + this._selectLines(this._currentMarker, this._selectionStart); + } + + public selectToNextCommand(): void { + if (this._selectionStart === null) { + this._selectionStart = this._currentMarker; + } + this.focusNextCommand(true); + // if (!this._currentMarker + this._selectLines(this._currentMarker, this._selectionStart); + } + + private _selectLines(start: IMarker | Boundary, end: IMarker | Boundary | null): void { + if (end === null) { + end = Boundary.Bottom; + } + + let startLine = this._getLine(start); + let endLine = this._getLine(end); + + if (startLine > endLine) { + const temp = startLine; + startLine = endLine; + endLine = temp; + } + + // Subtract a line as the marker is on the line the command run, we do not want the next + // command in the selection for the current command + endLine -= 1; + + this._xterm.selectLines(startLine, endLine); + } + + private _getLine(marker: IMarker | Boundary): number { + // Use the _second last_ row as the last row is likely the prompt + if (marker === Boundary.Bottom) { + return this._xterm.buffer.ybase + this._xterm.rows - 1; + } + + if (marker === Boundary.Top) { + return 0; + } + + return marker.line; + } +}