Proper copy and paste support for integrated terminal

Fixes #6451
This commit is contained in:
Daniel Imms 2016-07-11 15:13:13 -07:00
parent 562317d793
commit 9900f278ab
8 changed files with 106 additions and 17 deletions

View file

@ -96,7 +96,8 @@
{
"command": "markdown.showPreview",
"key": "shift+ctrl+v",
"mac": "shift+cmd+v"
"mac": "shift+cmd+v",
"when": "!terminalFocus"
},
{
"command": "markdown.showPreviewToSide",

View file

@ -22,6 +22,8 @@ import {IWorkbenchEditorService} from 'vs/workbench/services/editor/common/edito
import {asFileEditorInput} from 'vs/workbench/common/editor';
import {KeyMod, KeyCode} from 'vs/base/common/keyCodes';
import {Extensions, IConfigurationRegistry} from 'vs/platform/configuration/common/configurationRegistry';
import {KbExpr} from 'vs/platform/keybinding/common/keybinding';
import {KEYBINDING_CONTEXT_TERMINAL_FOCUS} from 'vs/workbench/parts/terminal/electron-browser/terminal';
import {DEFAULT_TERMINAL_WINDOWS, DEFAULT_TERMINAL_LINUX, DEFAULT_TERMINAL_OSX} from 'vs/workbench/parts/execution/electron-browser/terminal';
let configurationRegistry = <IConfigurationRegistry>Registry.as(Extensions.Configuration);
@ -132,7 +134,8 @@ actionBarRegistry.registerActionBarContributor(Scope.VIEWER, FileViewerActionCon
OpenConsoleAction,
OpenConsoleAction.ID,
OpenConsoleAction.Label,
{ primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_C }
{ primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_C },
KbExpr.and(KbExpr.not(KEYBINDING_CONTEXT_TERMINAL_FOCUS))
),
'Open New Command Prompt'
);

View file

@ -5,17 +5,18 @@
import 'vs/css!./media/terminal';
import 'vs/css!./media/xterm';
import nls = require('vs/nls');
import {KeyMod, KeyCode} from 'vs/base/common/keyCodes';
import {SyncActionDescriptor} from 'vs/platform/actions/common/actions';
import {registerSingleton} from 'vs/platform/instantiation/common/extensions';
import {IWorkbenchActionRegistry, Extensions as ActionExtensions} from 'vs/workbench/common/actionRegistry';
import {TerminalService} from 'vs/workbench/parts/terminal/electron-browser/terminalService';
import {KillTerminalAction, CreateNewTerminalAction, FocusTerminalAction, FocusNextTerminalAction, FocusPreviousTerminalAction, RunSelectedTextInTerminalAction, ToggleTerminalAction} from 'vs/workbench/parts/terminal/electron-browser/terminalActions';
import {ITerminalService, TERMINAL_PANEL_ID, TERMINAL_DEFAULT_SHELL_LINUX, TERMINAL_DEFAULT_SHELL_OSX, TERMINAL_DEFAULT_SHELL_WINDOWS} from 'vs/workbench/parts/terminal/electron-browser/terminal';
import * as panel from 'vs/workbench/browser/panel';
import {Registry} from 'vs/platform/platform';
import nls = require('vs/nls');
import {Extensions, IConfigurationRegistry} from 'vs/platform/configuration/common/configurationRegistry';
import {KbExpr} from 'vs/platform/keybinding/common/keybinding';
import {ITerminalService, KEYBINDING_CONTEXT_TERMINAL_FOCUS, TERMINAL_PANEL_ID, TERMINAL_DEFAULT_SHELL_LINUX, TERMINAL_DEFAULT_SHELL_OSX, TERMINAL_DEFAULT_SHELL_WINDOWS} from 'vs/workbench/parts/terminal/electron-browser/terminal';
import {IWorkbenchActionRegistry, Extensions as ActionExtensions} from 'vs/workbench/common/actionRegistry';
import {KeyCode, KeyMod} from 'vs/base/common/keyCodes';
import {KillTerminalAction, CopyTerminalSelectionAction, CreateNewTerminalAction, FocusTerminalAction, FocusNextTerminalAction, FocusPreviousTerminalAction, RunSelectedTextInTerminalAction, TerminalPasteAction, ToggleTerminalAction} from 'vs/workbench/parts/terminal/electron-browser/terminalActions';
import {Registry} from 'vs/platform/platform';
import {SyncActionDescriptor} from 'vs/platform/actions/common/actions';
import {TerminalService} from 'vs/workbench/parts/terminal/electron-browser/terminalService';
import {registerSingleton} from 'vs/platform/instantiation/common/extensions';
let configurationRegistry = <IConfigurationRegistry>Registry.as(Extensions.Configuration);
configurationRegistry.registerConfiguration({
@ -95,6 +96,9 @@ registerSingleton(ITerminalService, TerminalService);
// On mac cmd+` is reserved to cycle between windows, that's why the keybindings use WinCtrl
let actionRegistry = <IWorkbenchActionRegistry>Registry.as(ActionExtensions.WorkbenchActions);
actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(KillTerminalAction, KillTerminalAction.ID, KillTerminalAction.LABEL), KillTerminalAction.LABEL);
actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(CopyTerminalSelectionAction, CopyTerminalSelectionAction.ID, CopyTerminalSelectionAction.LABEL, {
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_C
}, KbExpr.and(KbExpr.has(KEYBINDING_CONTEXT_TERMINAL_FOCUS))), CopyTerminalSelectionAction.LABEL);
actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(CreateNewTerminalAction, CreateNewTerminalAction.ID, CreateNewTerminalAction.LABEL, {
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.US_BACKTICK,
mac: { primary: KeyMod.WinCtrl | KeyMod.Shift | KeyCode.US_BACKTICK }
@ -102,6 +106,9 @@ actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(CreateNewTermina
actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(FocusTerminalAction, FocusTerminalAction.ID, FocusTerminalAction.LABEL), FocusTerminalAction.LABEL);
actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(FocusNextTerminalAction, FocusNextTerminalAction.ID, FocusNextTerminalAction.LABEL), KillTerminalAction.LABEL);
actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(FocusPreviousTerminalAction, FocusPreviousTerminalAction.ID, FocusPreviousTerminalAction.LABEL), KillTerminalAction.LABEL);
actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(TerminalPasteAction, TerminalPasteAction.ID, TerminalPasteAction.LABEL, {
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_V
}, KbExpr.and(KbExpr.has(KEYBINDING_CONTEXT_TERMINAL_FOCUS))), CopyTerminalSelectionAction.LABEL);
actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(RunSelectedTextInTerminalAction, RunSelectedTextInTerminalAction.ID, RunSelectedTextInTerminalAction.LABEL), RunSelectedTextInTerminalAction.LABEL);
actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(ToggleTerminalAction, ToggleTerminalAction.ID, ToggleTerminalAction.LABEL, {
primary: KeyMod.CtrlCmd | KeyCode.US_BACKTICK,

View file

@ -20,6 +20,11 @@ export const TERMINAL_DEFAULT_SHELL_LINUX = !platform.isWindows ? (process.env.S
export const TERMINAL_DEFAULT_SHELL_OSX = !platform.isWindows ? (process.env.SHELL || 'sh') : 'sh';
export const TERMINAL_DEFAULT_SHELL_WINDOWS = processes.getWindowsShell();
/**
* A context key that is set when the integrated terminal has focus.
*/
export const KEYBINDING_CONTEXT_TERMINAL_FOCUS = 'terminalFocus';
export var ITerminalService = createDecorator<ITerminalService>(TERMINAL_SERVICE_ID);
export interface ITerminalConfiguration {
@ -55,11 +60,13 @@ export interface ITerminalService {
onInstanceTitleChanged: Event<string>;
close(): TPromise<any>;
copySelection(): TPromise<any>;
createNew(): TPromise<any>;
focus(): TPromise<any>;
focusNext(): TPromise<any>;
focusPrevious(): TPromise<any>;
hide(): TPromise<any>;
paste(): TPromise<any>;
runSelectedText(): TPromise<any>;
setActiveTerminal(index: number): TPromise<any>;
toggle(): TPromise<any>;

View file

@ -45,6 +45,27 @@ export class KillTerminalAction extends Action {
}
}
/**
* Copies the terminal selection. Note that since the command palette takes focus from the terminal,
* this can only be triggered via a keybinding.
*/
export class CopyTerminalSelectionAction extends Action {
public static ID = 'workbench.action.terminal.copySelection';
public static LABEL = nls.localize('workbench.action.terminal.copySelection', "Terminal: Copy Selection");
constructor(
id: string, label: string,
@ITerminalService private terminalService: ITerminalService
) {
super(id, label);
}
public run(event?: any): TPromise<any> {
return this.terminalService.copySelection();
}
}
export class CreateNewTerminalAction extends Action {
public static ID = 'workbench.action.terminal.new';
@ -114,6 +135,22 @@ export class FocusPreviousTerminalAction extends Action {
return this.terminalService.focusPrevious();
}
}
export class TerminalPasteAction extends Action {
public static ID = 'workbench.action.terminal.paste';
public static LABEL = nls.localize('workbench.action.terminal.paste', "Terminal: Paste into Active Terminal");
constructor(
id: string, label: string,
@ITerminalService private terminalService: ITerminalService
) {
super(id, label);
}
public run(event?: any): TPromise<any> {
return this.terminalService.paste();
}
}
export class RunSelectedTextInTerminalAction extends Action {

View file

@ -10,6 +10,7 @@ import os = require('os');
import platform = require('vs/base/common/platform');
import xterm = require('xterm');
import {Dimension} from 'vs/base/browser/builder';
import {IKeybindingContextKey} from 'vs/platform/keybinding/common/keybinding';
import {IMessageService, Severity} from 'vs/platform/message/common/message';
import {ITerminalFont} from 'vs/workbench/parts/terminal/electron-browser/terminalConfigHelper';
import {ITerminalProcess, ITerminalService} from 'vs/workbench/parts/terminal/electron-browser/terminal';
@ -33,6 +34,7 @@ export class TerminalInstance {
private contextService: IWorkspaceContextService,
private terminalService: ITerminalService,
private messageService: IMessageService,
private terminalFocusContextKey: IKeybindingContextKey<boolean>,
private onExitCallback: (TerminalInstance) => void
) {
this.toDispose = [];
@ -84,6 +86,15 @@ export class TerminalInstance {
}));
this.xterm.open(this.terminalDomElement);
let self = this;
this.toDispose.push(DOM.addDisposableListener(this.xterm.element, 'focus', (event: KeyboardEvent) => {
self.terminalFocusContextKey.set(true);
}));
this.toDispose.push(DOM.addDisposableListener(this.xterm.element, 'blur', (event: KeyboardEvent) => {
self.terminalFocusContextKey.reset();
}));
this.wrapperElement.appendChild(this.terminalDomElement);
this.parentDomElement.appendChild(this.wrapperElement);
}

View file

@ -12,6 +12,7 @@ import {Builder, Dimension} from 'vs/base/browser/builder';
import {KillTerminalAction, CreateNewTerminalAction, SwitchTerminalInstanceAction, SwitchTerminalInstanceActionItem} from 'vs/workbench/parts/terminal/electron-browser/terminalActions';
import {IActionItem} from 'vs/base/browser/ui/actionbar/actionbar';
import {IConfigurationService} from 'vs/platform/configuration/common/configuration';
import {IKeybindingContextKey} from 'vs/platform/keybinding/common/keybinding';
import {IInstantiationService} from 'vs/platform/instantiation/common/instantiation';
import {IMessageService} from 'vs/platform/message/common/message';
import {ITelemetryService} from 'vs/platform/telemetry/common/telemetry';
@ -98,8 +99,8 @@ export class TerminalPanel extends Panel {
return this.terminalService.createNew();
}
public createNewTerminalInstance(terminalProcess: ITerminalProcess): TPromise<void> {
return this.createTerminal(terminalProcess).then(() => {
public createNewTerminalInstance(terminalProcess: ITerminalProcess, terminalFocusContextKey: IKeybindingContextKey<boolean>): TPromise<void> {
return this.createTerminal(terminalProcess, terminalFocusContextKey).then(() => {
this.updateConfig();
this.focus();
});
@ -130,9 +131,9 @@ export class TerminalPanel extends Panel {
return super.setVisible(visible);
}
private createTerminal(terminalProcess: ITerminalProcess): TPromise<TerminalInstance> {
private createTerminal(terminalProcess: ITerminalProcess, terminalFocusContextKey: IKeybindingContextKey<boolean>): TPromise<TerminalInstance> {
return new TPromise<TerminalInstance>(resolve => {
var terminalInstance = new TerminalInstance(terminalProcess, this.terminalContainer, this.contextService, this.terminalService, this.messageService, this.onTerminalInstanceExit.bind(this));
var terminalInstance = new TerminalInstance(terminalProcess, this.terminalContainer, this.contextService, this.terminalService, this.messageService, terminalFocusContextKey, this.onTerminalInstanceExit.bind(this));
this.terminalInstances.push(terminalInstance);
this.setActiveTerminal(this.terminalInstances.length - 1);
this.toDispose.push(this.themeService.onDidThemeChange(this.updateTheme.bind(this)));

View file

@ -6,6 +6,7 @@
import URI from 'vs/base/common/uri';
import Event, {Emitter} from 'vs/base/common/event';
import cp = require('child_process');
import nls = require('vs/nls');
import os = require('os');
import path = require('path');
import platform = require('vs/base/common/platform');
@ -13,10 +14,12 @@ import {Builder} from 'vs/base/browser/builder';
import {EndOfLinePreference} from 'vs/editor/common/editorCommon';
import {ICodeEditorService} from 'vs/editor/common/services/codeEditorService';
import {IConfigurationService} from 'vs/platform/configuration/common/configuration';
import {IKeybindingContextKey, IKeybindingService} from 'vs/platform/keybinding/common/keybinding';
import {IMessageService, Severity} from 'vs/platform/message/common/message';
import {IPanelService} from 'vs/workbench/services/panel/common/panelService';
import {IPartService} from 'vs/workbench/services/part/common/partService';
import {IStringDictionary} from 'vs/base/common/collections';
import {ITerminalProcess, ITerminalService, TERMINAL_PANEL_ID} from 'vs/workbench/parts/terminal/electron-browser/terminal';
import {ITerminalProcess, ITerminalService, KEYBINDING_CONTEXT_TERMINAL_FOCUS, TERMINAL_PANEL_ID} from 'vs/workbench/parts/terminal/electron-browser/terminal';
import {IWorkspaceContextService} from 'vs/platform/workspace/common/workspace';
import {TPromise} from 'vs/base/common/winjs.base';
import {TerminalConfigHelper} from 'vs/workbench/parts/terminal/electron-browser/terminalConfigHelper';
@ -27,6 +30,7 @@ export class TerminalService implements ITerminalService {
private activeTerminalIndex: number = 0;
private terminalProcesses: ITerminalProcess[] = [];
protected _terminalFocusContextKey: IKeybindingContextKey<boolean>;
private configHelper: TerminalConfigHelper;
private _onActiveInstanceChanged: Emitter<string>;
@ -36,6 +40,8 @@ export class TerminalService implements ITerminalService {
constructor(
@ICodeEditorService private codeEditorService: ICodeEditorService,
@IConfigurationService private configurationService: IConfigurationService,
@IKeybindingService private keybindingService: IKeybindingService,
@IMessageService private messageService: IMessageService,
@IPanelService private panelService: IPanelService,
@IPartService private partService: IPartService,
@IWorkspaceContextService private contextService: IWorkspaceContextService
@ -43,6 +49,7 @@ export class TerminalService implements ITerminalService {
this._onActiveInstanceChanged = new Emitter<string>();
this._onInstancesChanged = new Emitter<string>();
this._onInstanceTitleChanged = new Emitter<string>();
this._terminalFocusContextKey = this.keybindingService.createKey(KEYBINDING_CONTEXT_TERMINAL_FOCUS, undefined);
}
public get onActiveInstanceChanged(): Event<string> {
@ -160,7 +167,7 @@ export class TerminalService implements ITerminalService {
}
self.initConfigHelper(terminalPanel.getContainer());
terminalPanel.createNewTerminalInstance(self.createTerminalProcess());
terminalPanel.createNewTerminalInstance(self.createTerminalProcess(), this._terminalFocusContextKey);
self._onInstancesChanged.fire();
return TPromise.as(void 0);
});
@ -172,6 +179,21 @@ export class TerminalService implements ITerminalService {
});
}
public copySelection(): TPromise<any> {
if (document.activeElement.classList.contains('xterm')) {
document.execCommand('copy');
} else {
this.messageService.show(Severity.Warning, nls.localize('terminal.integrated.copySelection.noSelection', 'Cannot copy terminal selection when terminal does not have focus'));
}
return TPromise.as(void 0);
}
public paste(): TPromise<any> {
return this.showAndGetTerminalPanel().then((terminalPanel) => {
document.execCommand('paste');
});
}
private showAndGetTerminalPanel(): TPromise<TerminalPanel> {
return new TPromise<TerminalPanel>((complete) => {
let panel = this.panelService.getActivePanel();