Merge pull request #43863 from Microsoft/tyriar/split_simplify

Tyriar/split simplify
This commit is contained in:
Daniel Imms 2018-02-16 14:04:36 -08:00 committed by GitHub
commit e64896d4cb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 341 additions and 299 deletions

View file

@ -163,7 +163,7 @@ export interface ITerminalService {
setActiveInstance(terminalInstance: ITerminalInstance): void;
setActiveInstanceByIndex(terminalIndex: number): void;
getActiveOrCreateInstance(wasNewTerminalAction?: boolean): ITerminalInstance;
splitInstanceVertically(instance: ITerminalInstance): void;
splitInstance(instance: ITerminalInstance): void;
getActiveTab(): ITerminalTab;
setActiveTabToNext(): void;
@ -196,7 +196,9 @@ export interface ITerminalTab {
onDisposed: Event<ITerminalTab>;
onInstancesChanged: Event<void>;
focusDirection(direction: Direction): void;
focusPreviousPane(): void;
focusNextPane(): void;
resizePane(direction: Direction): void;
setActiveInstanceByIndex(index: number): void;
attachToElement(element: HTMLElement): void;
setVisible(visible: boolean): void;

View file

@ -230,7 +230,7 @@ export abstract class TerminalService implements ITerminalService {
this.setActiveTabByIndex(newIndex);
}
public splitInstanceVertically(instanceToSplit: ITerminalInstance): void {
public splitInstance(instanceToSplit: ITerminalInstance): void {
const tab = this._getTabForInstance(instanceToSplit);
if (!tab) {
return;

View file

@ -76,11 +76,15 @@
box-sizing: border-box;
}
.monaco-workbench .panel.integrated-terminal .split-view-view:not(:first-child) {
/* border-color is set by theme key terminal.border */
/* border-color is set by theme key terminal.border */
.monaco-workbench .panel.integrated-terminal .monaco-split-view2.horizontal .split-view-view:not(:first-child) {
border-left-width: 1px;
border-left-style: solid;
}
.monaco-workbench .panel.integrated-terminal .monaco-split-view2.vertical .split-view-view:not(:first-child) {
border-top-width: 1px;
border-top-style: solid;
}
.monaco-workbench .panel.integrated-terminal.enable-ligatures {
font-variant-ligatures: normal;

View file

@ -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, SplitVerticalTerminalAction, FocusTerminalLeftAction, FocusTerminalRightAction, FocusTerminalUpAction, FocusTerminalDownAction } 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 } 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';
@ -273,9 +273,13 @@ configurationRegistry.registerConfiguration({
MoveToLineEndTerminalAction.ID,
TogglePanelAction.ID,
'workbench.action.quickOpenView',
SplitVerticalTerminalAction.ID,
FocusTerminalLeftAction.ID,
FocusTerminalRightAction.ID
SplitTerminalAction.ID,
FocusPreviousPaneTerminalAction.ID,
FocusNextPaneTerminalAction.ID,
ResizePaneLeftTerminalAction.ID,
ResizePaneRightTerminalAction.ID,
ResizePaneUpTerminalAction.ID,
ResizePaneDownTerminalAction.ID
].sort()
},
'terminal.integrated.env.osx': {
@ -412,26 +416,42 @@ actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(MoveToLineEndTer
primary: null,
mac: { primary: KeyMod.CtrlCmd | KeyCode.RightArrow }
}, KEYBINDING_CONTEXT_TERMINAL_FOCUS), 'Terminal: Move To Line End', category);
actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(SplitVerticalTerminalAction, SplitVerticalTerminalAction.ID, SplitVerticalTerminalAction.LABEL, {
actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(SplitTerminalAction, SplitTerminalAction.ID, SplitTerminalAction.LABEL, {
primary: null,
mac: { primary: KeyMod.CtrlCmd | KeyCode.KEY_D }
}, KEYBINDING_CONTEXT_TERMINAL_FOCUS), 'Terminal: Split Vertically', category);
actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(FocusTerminalLeftAction, FocusTerminalLeftAction.ID, FocusTerminalLeftAction.LABEL, {
}, KEYBINDING_CONTEXT_TERMINAL_FOCUS), 'Terminal: Split', category);
actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(FocusPreviousPaneTerminalAction, FocusPreviousPaneTerminalAction.ID, FocusPreviousPaneTerminalAction.LABEL, {
primary: KeyMod.Alt | KeyCode.LeftArrow,
mac: { primary: KeyMod.Alt | KeyMod.CtrlCmd | KeyCode.LeftArrow }
}, KEYBINDING_CONTEXT_TERMINAL_FOCUS), 'Terminal: Focus Terminal To Left', category);
actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(FocusTerminalRightAction, FocusTerminalRightAction.ID, FocusTerminalRightAction.LABEL, {
secondary: [KeyMod.Alt | KeyCode.UpArrow],
mac: {
primary: KeyMod.Alt | KeyMod.CtrlCmd | KeyCode.LeftArrow,
secondary: [KeyMod.Alt | KeyMod.CtrlCmd | KeyCode.UpArrow]
}
}, KEYBINDING_CONTEXT_TERMINAL_FOCUS), 'Terminal: Focus Previous Pane', category);
actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(FocusNextPaneTerminalAction, FocusNextPaneTerminalAction.ID, FocusNextPaneTerminalAction.LABEL, {
primary: KeyMod.Alt | KeyCode.RightArrow,
mac: { primary: KeyMod.Alt | KeyMod.CtrlCmd | KeyCode.RightArrow }
}, KEYBINDING_CONTEXT_TERMINAL_FOCUS), 'Terminal: Focus Terminal To Right', category);
actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(FocusTerminalDownAction, FocusTerminalDownAction.ID, FocusTerminalDownAction.LABEL, {
primary: KeyMod.Alt | KeyCode.DownArrow,
mac: { primary: KeyMod.Alt | KeyMod.CtrlCmd | KeyCode.DownArrow }
}, KEYBINDING_CONTEXT_TERMINAL_FOCUS), 'Terminal: Focus Terminal Below', category);
actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(FocusTerminalUpAction, FocusTerminalUpAction.ID, FocusTerminalUpAction.LABEL, {
primary: KeyMod.Alt | KeyCode.UpArrow,
mac: { primary: KeyMod.Alt | KeyMod.CtrlCmd | KeyCode.UpArrow }
}, KEYBINDING_CONTEXT_TERMINAL_FOCUS), 'Terminal: Focus Terminal Above', category);
secondary: [KeyMod.Alt | KeyCode.DownArrow],
mac: {
primary: KeyMod.Alt | KeyMod.CtrlCmd | KeyCode.RightArrow,
secondary: [KeyMod.Alt | KeyMod.CtrlCmd | KeyCode.DownArrow]
}
}, KEYBINDING_CONTEXT_TERMINAL_FOCUS), 'Terminal: Focus Next Pane', category);
actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(ResizePaneLeftTerminalAction, ResizePaneLeftTerminalAction.ID, ResizePaneLeftTerminalAction.LABEL, {
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.LeftArrow,
mac: { primary: KeyMod.CtrlCmd | KeyMod.WinCtrl | KeyCode.LeftArrow }
}, KEYBINDING_CONTEXT_TERMINAL_FOCUS), 'Terminal: Resize Pane Left', category);
actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(ResizePaneRightTerminalAction, ResizePaneRightTerminalAction.ID, ResizePaneRightTerminalAction.LABEL, {
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.RightArrow,
mac: { primary: KeyMod.CtrlCmd | KeyMod.WinCtrl | KeyCode.RightArrow }
}, KEYBINDING_CONTEXT_TERMINAL_FOCUS), 'Terminal: Resize Pane Right', category);
actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(ResizePaneUpTerminalAction, ResizePaneUpTerminalAction.ID, ResizePaneUpTerminalAction.LABEL, {
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.UpArrow,
mac: { primary: KeyMod.CtrlCmd | KeyMod.WinCtrl | KeyCode.UpArrow }
}, KEYBINDING_CONTEXT_TERMINAL_FOCUS), 'Terminal: Resize Pane Up', category);
actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(ResizePaneDownTerminalAction, ResizePaneDownTerminalAction.ID, ResizePaneDownTerminalAction.LABEL, {
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.DownArrow,
mac: { primary: KeyMod.CtrlCmd | KeyMod.WinCtrl | KeyCode.DownArrow }
}, KEYBINDING_CONTEXT_TERMINAL_FOCUS), 'Terminal: Resize Pane Down', category);
terminalCommands.setup();

View file

@ -296,9 +296,9 @@ export class CreateNewInActiveWorkspaceTerminalAction extends Action {
}
}
export class SplitVerticalTerminalAction extends Action {
public static readonly ID = 'workbench.action.terminal.splitVertical';
public static readonly LABEL = nls.localize('workbench.action.terminal.splitVertical', "Split the terminal vertically");
export class SplitTerminalAction extends Action {
public static readonly ID = 'workbench.action.terminal.split';
public static readonly LABEL = nls.localize('workbench.action.terminal.split', "Split Terminal");
constructor(
id: string, label: string,
@ -312,7 +312,49 @@ export class SplitVerticalTerminalAction extends Action {
if (!instance) {
return TPromise.as(void 0);
}
this._terminalService.splitInstanceVertically(instance);
this._terminalService.splitInstance(instance);
return this._terminalService.showPanel(true);
}
}
export class FocusPreviousPaneTerminalAction extends Action {
public static readonly ID = 'workbench.action.terminal.focusPreviousPane';
public static readonly LABEL = nls.localize('workbench.action.terminal.focusPreviousPane', "Focus Previous Pane");
constructor(
id: string, label: string,
@ITerminalService private readonly _terminalService: ITerminalService
) {
super(id, label);
}
public run(event?: any): TPromise<any> {
const tab = this._terminalService.getActiveTab();
if (!tab) {
return TPromise.as(void 0);
}
tab.focusPreviousPane();
return this._terminalService.showPanel(true);
}
}
export class FocusNextPaneTerminalAction extends Action {
public static readonly ID = 'workbench.action.terminal.focusNextPane';
public static readonly LABEL = nls.localize('workbench.action.terminal.focusNextPane', "Focus Next Pane");
constructor(
id: string, label: string,
@ITerminalService private readonly _terminalService: ITerminalService
) {
super(id, label);
}
public run(event?: any): TPromise<any> {
const tab = this._terminalService.getActiveTab();
if (!tab) {
return TPromise.as(void 0);
}
tab.focusNextPane();
return this._terminalService.showPanel(true);
}
}
@ -328,46 +370,57 @@ export abstract class BaseFocusDirectionTerminalAction extends Action {
public run(event?: any): TPromise<any> {
const tab = this._terminalService.getActiveTab();
if (!tab) {
return TPromise.as(void 0);
if (tab) {
tab.resizePane(this._direction);
}
tab.focusDirection(this._direction);
return this._terminalService.showPanel(true);
return TPromise.as(void 0);
}
}
export class FocusTerminalLeftAction extends BaseFocusDirectionTerminalAction {
public static readonly ID = 'workbench.action.terminal.focusTerminalLeft';
public static readonly LABEL = nls.localize('workbench.action.terminal.focusTerminalLeft', "Focus terminal to the left");
export class ResizePaneLeftTerminalAction extends BaseFocusDirectionTerminalAction {
public static readonly ID = 'workbench.action.terminal.resizePaneLeft';
public static readonly LABEL = nls.localize('workbench.action.terminal.resizePaneLeft', "Resize Pane Left");
constructor(id: string, label: string, @ITerminalService terminalService: ITerminalService) {
constructor(
id: string, label: string,
@ITerminalService readonly terminalService: ITerminalService
) {
super(id, label, Direction.Left, terminalService);
}
}
export class FocusTerminalRightAction extends BaseFocusDirectionTerminalAction {
public static readonly ID = 'workbench.action.terminal.focusTerminalRight';
public static readonly LABEL = nls.localize('workbench.action.terminal.focusTerminalRight', "Focus terminal to the right");
export class ResizePaneRightTerminalAction extends BaseFocusDirectionTerminalAction {
public static readonly ID = 'workbench.action.terminal.resizePaneRight';
public static readonly LABEL = nls.localize('workbench.action.terminal.resizePaneRight', "Resize Pane Right");
constructor(id: string, label: string, @ITerminalService terminalService: ITerminalService) {
constructor(
id: string, label: string,
@ITerminalService readonly terminalService: ITerminalService
) {
super(id, label, Direction.Right, terminalService);
}
}
export class FocusTerminalUpAction extends BaseFocusDirectionTerminalAction {
public static readonly ID = 'workbench.action.terminal.focusTerminalUp';
public static readonly LABEL = nls.localize('workbench.action.terminal.focusTerminalUp', "Focus terminal above");
export class ResizePaneUpTerminalAction extends BaseFocusDirectionTerminalAction {
public static readonly ID = 'workbench.action.terminal.resizePaneUp';
public static readonly LABEL = nls.localize('workbench.action.terminal.resizePaneUp', "Resize Pane Up");
constructor(id: string, label: string, @ITerminalService terminalService: ITerminalService) {
constructor(
id: string, label: string,
@ITerminalService readonly terminalService: ITerminalService
) {
super(id, label, Direction.Up, terminalService);
}
}
export class FocusTerminalDownAction extends BaseFocusDirectionTerminalAction {
public static readonly ID = 'workbench.action.terminal.focusTerminalDown';
public static readonly LABEL = nls.localize('workbench.action.terminal.focusTerminalDown', "Focus terminal below");
export class ResizePaneDownTerminalAction extends BaseFocusDirectionTerminalAction {
public static readonly ID = 'workbench.action.terminal.resizePaneDown';
public static readonly LABEL = nls.localize('workbench.action.terminal.resizePaneDown', "Resize Pane Down");
constructor(id: string, label: string, @ITerminalService terminalService: ITerminalService) {
constructor(
id: string, label: string,
@ITerminalService readonly terminalService: ITerminalService
) {
super(id, label, Direction.Down, terminalService);
}
}

View file

@ -168,7 +168,7 @@ export class TerminalInstance implements ITerminalInstance {
// Only attach xterm.js to the DOM if the terminal panel has been opened before.
if (_container) {
this.attachToElement(_container);
this._attachToElement(_container);
}
});
@ -337,6 +337,25 @@ export class TerminalInstance implements ITerminalInstance {
}
public attachToElement(container: HTMLElement): void {
// The container did not change, do nothing
if (this._container === container) {
return;
}
// Attach has not occured yet
if (!this._wrapperElement) {
this._attachToElement(container);
return;
}
// TODO: Verify listeners still work
// The container changed, reattach
this._container.removeChild(this._wrapperElement);
this._container = container;
this._container.appendChild(this._wrapperElement);
}
public _attachToElement(container: HTMLElement): void {
this._xtermReadyPromise.then(() => {
if (this._wrapperElement) {
throw new Error('The terminal instance has already been attached to a container');

View file

@ -19,7 +19,7 @@ import { ITerminalService, TERMINAL_PANEL_ID } from 'vs/workbench/parts/terminal
import { IThemeService, ITheme } from 'vs/platform/theme/common/themeService';
import { TerminalFindWidget } from './terminalFindWidget';
import { editorHoverBackground, editorHoverBorder, editorForeground } from 'vs/platform/theme/common/colorRegistry';
import { KillTerminalAction, SwitchTerminalAction, SwitchTerminalActionItem, CopyTerminalSelectionAction, TerminalPasteAction, ClearTerminalAction, SelectAllTerminalAction, CreateNewTerminalAction, SplitVerticalTerminalAction } from 'vs/workbench/parts/terminal/electron-browser/terminalActions';
import { KillTerminalAction, SwitchTerminalAction, SwitchTerminalActionItem, CopyTerminalSelectionAction, TerminalPasteAction, ClearTerminalAction, SelectAllTerminalAction, CreateNewTerminalAction, SplitTerminalAction } from 'vs/workbench/parts/terminal/electron-browser/terminalActions';
import { Panel } from 'vs/workbench/browser/panel';
import { StandardMouseEvent } from 'vs/base/browser/mouseEvent';
import { TPromise } from 'vs/base/common/winjs.base';
@ -149,7 +149,7 @@ export class TerminalPanel extends Panel {
new Separator(),
this._instantiationService.createInstance(ClearTerminalAction, ClearTerminalAction.ID, nls.localize('clear', "Clear")),
new Separator(),
this._instantiationService.createInstance(SplitVerticalTerminalAction, SplitVerticalTerminalAction.ID, nls.localize('splitVertically', "Split Vertically"))
this._instantiationService.createInstance(SplitTerminalAction, SplitTerminalAction.ID, nls.localize('splitVertically', "Split Vertically"))
];
this._contextMenuActions.forEach(a => {
this._register(a);
@ -298,7 +298,7 @@ export class TerminalPanel extends Panel {
const borderColor = theme.getColor(TERMINAL_BORDER_COLOR) || theme.getColor(PANEL_BORDER);
if (borderColor) {
css += `.monaco-workbench .panel.integrated-terminal .split-view-view:not(:first-child) { border-left-color: ${borderColor.toString()}; }`;
css += `.monaco-workbench .panel.integrated-terminal .split-view-view:not(:first-child) { border-color: ${borderColor.toString()}; }`;
}
// Borrow the editor's hover background for now

View file

@ -237,8 +237,5 @@ export class TerminalService extends AbstractTerminalService implements ITermina
this._configHelper.panelContainer = panelContainer;
this._terminalContainer = terminalContainer;
this._terminalTabs.forEach(tab => tab.attachToElement(this._terminalContainer));
this._terminalInstances.forEach(terminalInstance => {
terminalInstance.attachToElement(this._terminalContainer);
});
}
}

View file

@ -3,7 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ITerminalInstance, IShellLaunchConfig, ITerminalTab, Direction } from 'vs/workbench/parts/terminal/common/terminal';
import { ITerminalInstance, IShellLaunchConfig, ITerminalTab, Direction, ITerminalService } from 'vs/workbench/parts/terminal/common/terminal';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { TerminalConfigHelper } from 'vs/workbench/parts/terminal/electron-browser/terminalConfigHelper';
import { IContextKey } from 'vs/platform/contextkey/common/contextkey';
@ -11,61 +11,97 @@ import { TerminalInstance } from 'vs/workbench/parts/terminal/electron-browser/t
import Event, { Emitter, anyEvent } from 'vs/base/common/event';
import { IDisposable, Disposable } from 'vs/base/common/lifecycle';
import { SplitView, Orientation, IView } from 'vs/base/browser/ui/splitview/splitview';
import { IPartService, Position } from 'vs/workbench/services/part/common/partService';
class SplitPane implements IView {
public minimumSize: number = 100;
public maximumSize: number = Number.MAX_VALUE;
public instance: ITerminalInstance;
public orientation: Orientation | undefined;
protected _size: number;
private _splitView: SplitView | undefined;
class SplitPaneContainer {
private _height: number;
private _width: number;
private _splitView: SplitView;
private _children: SplitPane[] = [];
private _container: HTMLElement;
private _isContainerSet: boolean = false;
private _onDidChange: Event<number | undefined> = Event.None;
public get children(): SplitPane[] { return this._children; }
// If the user sizes the panes manually, the proportional resizing will not be applied.
// Proportional resizing will come back when: a sash is reset, an instance is added/removed or
// the panel position moves.
private _isManuallySized: boolean = false;
private _onDidChange: Event<number | undefined> = Event.None;
public get onDidChange(): Event<number | undefined> { return this._onDidChange; }
constructor(
private _parent?: SplitPane,
public orthogonalSize?: number,
private _needsReattach?: boolean
private _container: HTMLElement,
public orientation: Orientation
) {
}
protected branch(container: HTMLElement, orientation: Orientation, instance: ITerminalInstance): void {
this.orientation = orientation;
container.removeChild((<any>this.instance)._wrapperElement);
this._splitView = new SplitView(container, { orientation });
this._width = this._container.offsetWidth;
this._height = this._container.offsetHeight;
this._splitView = new SplitView(this._container, { orientation: this.orientation });
// TODO: Dispose listeners
this._splitView.onDidSashReset(() => this._resetSize());
this.layout(this._size);
this.orthogonalLayout(this.orthogonalSize);
this.addChild(this.orthogonalSize / 2, this._size, this.instance, 0, this._isContainerSet);
this.addChild(this.orthogonalSize / 2, this._size, instance);
// Instance is now owned by the first child
this.instance = null;
this._splitView.onDidSashChange(() => this._isManuallySized = true);
this._splitView.layout(this.orientation === Orientation.HORIZONTAL ? this._width : this._height);
}
public split(instance: ITerminalInstance): void {
if (this._parent && this._parent.orientation === this.orientation) {
// TODO: Splitting sizes can be a bit weird when not splitting the right-most pane
// If we kept proportions when adding the view to the splitview it would be alright
const index = this._parent._children.indexOf(this);
this._parent.addChild(this._size / 2, this.orthogonalSize, instance, index + 1);
} else {
this.branch(this._container, this.orientation, instance);
public split(instance: ITerminalInstance, index: number = this._children.length): void {
const size = this.orientation === Orientation.HORIZONTAL ? this._width : this._height;
this._addChild(size / (this._children.length + 1), instance, index);
}
private _resetSize(): void {
// TODO: Optimize temrinal instance layout
let totalSize = 0;
for (let i = 0; i < this._splitView.length; i++) {
totalSize += this._splitView.getViewSize(i);
}
const newSize = Math.floor(totalSize / this._splitView.length);
for (let i = 0; i < this._splitView.length - 1; i++) {
this._splitView.resizeView(i, newSize);
}
this._isManuallySized = false;
}
public resizePane(index: number, direction: Direction, amount: number): void {
// TODO: Should resize pane up/down resize the panel?
// Only resize the correct dimension
const isHorizontal = direction === Direction.Left || direction === Direction.Right;
if (isHorizontal && this.orientation !== Orientation.HORIZONTAL ||
!isHorizontal && this.orientation !== Orientation.VERTICAL) {
return;
}
// Only resize when there is mor ethan one pane
if (this._children.length <= 1) {
return;
}
// Get sizes
const sizes = [];
for (let i = 0; i < this._splitView.length; i++) {
sizes.push(this._splitView.getViewSize(i));
}
// Remove size from right pane, unless index is the last pane in which case use left pane
const isSizingEndPane = index !== this._children.length - 1;
const indexToChange = isSizingEndPane ? index + 1 : index - 1;
if (isSizingEndPane && direction === Direction.Left) {
amount *= -1;
} else if (!isSizingEndPane && direction === Direction.Right) {
amount *= -1;
} else if (isSizingEndPane && direction === Direction.Up) {
amount *= -1;
} else if (!isSizingEndPane && direction === Direction.Down) {
amount *= -1;
}
sizes[index] += amount;
sizes[indexToChange] -= amount;
// Apply
for (let i = 0; i < this._splitView.length - 1; i++) {
this._splitView.resizeView(i, sizes[i]);
}
}
private addChild(size: number, orthogonalSize: number, instance: ITerminalInstance, index?: number, needsReattach?: boolean): void {
const child = new SplitPane(this, orthogonalSize, needsReattach);
private _addChild(size: number, instance: ITerminalInstance, index: number): void {
const child = new SplitPane(this.orientation === Orientation.HORIZONTAL ? this._height : this._width);
child.orientation = this.orientation;
child.instance = instance;
this._splitView.addView(child, size, index);
@ -76,47 +112,87 @@ class SplitPane implements IView {
this._children.push(child);
}
this._resetSize();
this._onDidChange = anyEvent(...this._children.map(c => c.onDidChange));
}
private _resetSize(): void {
let totalSize = 0;
for (let i = 0; i < this._splitView.length; i++) {
totalSize += this._splitView.getViewSize(i);
public remove(instance: ITerminalInstance): void {
let index = null;
for (let i = 0; i < this._children.length; i++) {
if (this._children[i].instance === instance) {
index = i;
}
}
const newSize = Math.floor(totalSize / this._splitView.length);
for (let i = 0; i < this._splitView.length - 1; i++) {
this._splitView.resizeView(i, newSize);
if (index !== null) {
this._children.splice(index, 1);
this._splitView.removeView(index);
this._resetSize();
}
}
public remove(): void {
if (!this._parent) {
public layout(width: number, height: number): void {
if (!this._isManuallySized) {
this._resetSize();
}
this._width = width;
this._height = height;
if (this.orientation === Orientation.HORIZONTAL) {
this._splitView.layout(width);
this._children.forEach(c => c.orthogonalLayout(height));
} else {
this._splitView.layout(height);
this._children.forEach(c => c.orthogonalLayout(width));
}
}
public setOrientation(orientation: Orientation): void {
if (this.orientation === orientation) {
return;
}
this.orientation = orientation;
this._parent.removeChild(this);
// Remove old split view
while (this._container.children.length > 0) {
this._container.removeChild(this._container.children[0]);
}
this._splitView.dispose();
// Create new split view with updated orientation
this._splitView = new SplitView(this._container, { orientation });
this._splitView.onDidSashReset(() => this._resetSize());
this._children.forEach(child => {
child.orientation = orientation;
this._splitView.addView(child, 1);
});
// Allow time for a layout to occur
this.layout(this._container.offsetWidth, this._container.offsetHeight);
this._resetSize();
}
}
public removeChild(child: SplitPane): void {
const index = this._children.indexOf(child);
this._children.splice(index, 1);
this._splitView.removeView(index);
class SplitPane implements IView {
public minimumSize: number = 40;
public maximumSize: number = Number.MAX_VALUE;
public instance: ITerminalInstance;
public orientation: Orientation | undefined;
protected _size: number;
private _onDidChange: Event<number | undefined> = Event.None;
public get onDidChange(): Event<number | undefined> { return this._onDidChange; }
constructor(
public orthogonalSize: number
) {
}
public render(container: HTMLElement): void {
if (!container) {
return;
}
this._container = container;
if (!this._isContainerSet && this.instance) {
if (this._needsReattach) {
(<any>this.instance).reattachToElement(container);
} else {
this.instance.attachToElement(container);
}
this._isContainerSet = true;
}
this.instance.attachToElement(container);
}
public layout(size: number): void {
@ -126,76 +202,23 @@ class SplitPane implements IView {
return;
}
if (this.instance) {
if (this.orientation === Orientation.VERTICAL) {
this.instance.layout({ width: this.orthogonalSize, height: this._size });
} else {
this.instance.layout({ width: this._size, height: this.orthogonalSize });
}
return;
}
for (const child of this.children) {
child.orthogonalLayout(this._size);
if (this.orientation === Orientation.VERTICAL) {
this.instance.layout({ width: this.orthogonalSize, height: this._size });
} else {
this.instance.layout({ width: this._size, height: this.orthogonalSize });
}
}
public orthogonalLayout(size: number): void {
this.orthogonalSize = size;
if (this._splitView) {
this._splitView.layout(this.orthogonalSize);
}
}
}
class RootSplitPane extends SplitPane {
private static _lastKnownWidth: number;
private static _lastKnownHeight: number;
private _width: number;
private _height: number;
protected branch(container: HTMLElement, orientation: Orientation, instance: ITerminalInstance): void {
if (orientation === Orientation.VERTICAL) {
this._size = this._width || RootSplitPane._lastKnownWidth;
this.orthogonalSize = this._height || RootSplitPane._lastKnownHeight;
} else {
this._size = this._height || RootSplitPane._lastKnownHeight;
this.orthogonalSize = this._width || RootSplitPane._lastKnownWidth;
}
super.branch(container, orientation, instance);
}
public layoutBox(width: number, height: number): void {
RootSplitPane._lastKnownWidth = width;
RootSplitPane._lastKnownHeight = height;
if (this.orientation === Orientation.VERTICAL) {
this.layout(width);
this.orthogonalLayout(height);
} else if (this.orientation === Orientation.HORIZONTAL) {
this.layout(height);
this.orthogonalLayout(width);
} else {
this._width = width;
this._height = height;
this.instance.layout({ width, height });
}
}
}
const directionOrientation: { [direction: number]: Orientation } = {
[Direction.Left]: Orientation.HORIZONTAL,
[Direction.Right]: Orientation.HORIZONTAL,
[Direction.Up]: Orientation.VERTICAL,
[Direction.Down]: Orientation.VERTICAL
};
export class TerminalTab extends Disposable implements ITerminalTab {
private _terminalInstances: ITerminalInstance[] = [];
private _rootSplitPane: RootSplitPane;
private _splitPaneContainer: SplitPaneContainer | undefined;
private _tabElement: HTMLElement;
private _panelPosition: Position = Position.BOTTOM;
private _activeInstanceIndex: number;
@ -211,7 +234,9 @@ export class TerminalTab extends Disposable implements ITerminalTab {
configHelper: TerminalConfigHelper,
private _container: HTMLElement,
shellLaunchConfig: IShellLaunchConfig,
@IInstantiationService private readonly _instantiationService: IInstantiationService
@IInstantiationService private readonly _instantiationService: IInstantiationService,
@ITerminalService private readonly _terminalService: ITerminalService,
@IPartService private readonly _partService: IPartService
) {
super();
this._onDisposed = new Emitter<ITerminalTab>();
@ -226,9 +251,6 @@ export class TerminalTab extends Disposable implements ITerminalTab {
this._initInstanceListeners(instance);
this._activeInstanceIndex = 0;
this._rootSplitPane = new RootSplitPane();
this._rootSplitPane.instance = instance;
if (this._container) {
this.attachToElement(this._container);
}
@ -272,10 +294,9 @@ export class TerminalTab extends Disposable implements ITerminalTab {
this.activeInstance.focus(true);
}
// Find the instance's SplitPane and unsplit it
const pane = this._findSplitPane(instance);
if (pane) {
pane.remove();
// Remove the instance from the split pane if it has been created
if (this._splitPaneContainer) {
this._splitPaneContainer.remove(instance);
}
// Fire events and dispose tab if it was the last instance
@ -286,44 +307,6 @@ export class TerminalTab extends Disposable implements ITerminalTab {
}
}
private _findSplitPane(instance: ITerminalInstance): SplitPane {
const openList: SplitPane[] = [this._rootSplitPane];
while (openList.length > 0) {
const current = openList.shift();
if (current.instance === instance) {
return current;
}
openList.push.apply(openList, current.children);
}
return null;
}
// TODO: Should this live inside SplitPane?
private _findSplitPanePath(instance: ITerminalInstance, path: SplitPane[] = [this._rootSplitPane]): SplitPane[] {
// Gets all split panes from the root to the pane containing the instance.
const pane = path[path.length - 1];
// Base case: path found
if (pane.instance === instance) {
return path;
}
// Rescurse child panes
for (let i = 0; i < pane.children.length; i++) {
const child = pane.children[i];
const subPath = path.slice();
subPath.push(child);
const result = this._findSplitPanePath(instance, subPath);
if (result) {
return result;
}
}
// No children contain instance
return null;
}
private _setActiveInstance(instance: ITerminalInstance): void {
this.setActiveInstanceByIndex(this._getIndexFromId(instance.id));
}
@ -343,7 +326,7 @@ export class TerminalTab extends Disposable implements ITerminalTab {
public setActiveInstanceByIndex(index: number): void {
// Check for invalid value
if (index >= this._terminalInstances.length) {
if (index < 0 || index >= this._terminalInstances.length) {
return;
}
@ -360,7 +343,12 @@ export class TerminalTab extends Disposable implements ITerminalTab {
this._tabElement = document.createElement('div');
this._tabElement.classList.add('terminal-tab');
this._container.appendChild(this._tabElement);
this._rootSplitPane.render(this._tabElement);
if (!this._splitPaneContainer) {
this._panelPosition = this._partService.getPanelPosition();
const orientation = this._panelPosition === Position.BOTTOM ? Orientation.HORIZONTAL : Orientation.VERTICAL;
this._splitPaneContainer = new SplitPaneContainer(this._tabElement, orientation);
this.terminalInstances.forEach(instance => this._splitPaneContainer.split(instance));
}
}
public get title(): string {
@ -388,25 +376,14 @@ export class TerminalTab extends Disposable implements ITerminalTab {
configHelper,
undefined,
shellLaunchConfig);
// TODO: Should this be pulled from the splitpanes instead? Currently there are 2 sources of truth.
// _terminalInstances is also the order they were created, not the order in which they appear
this._terminalInstances.push(instance);
this._terminalInstances.splice(this._activeInstanceIndex + 1, 0, instance);
this._initInstanceListeners(instance);
if (this._rootSplitPane.instance) {
this._rootSplitPane.orientation = Orientation.HORIZONTAL;
this._rootSplitPane.split(instance);
} else {
// The original branch has already occured, find the inner SplitPane and split it
const activePane = this._findSplitPane(this.activeInstance);
activePane.orientation = Orientation.HORIZONTAL;
activePane.split(instance);
}
if (this._tabElement) {
this._rootSplitPane.render(this._tabElement);
}
this._setActiveInstance(instance);
if (this._splitPaneContainer) {
this._splitPaneContainer.split(instance, this._activeInstanceIndex);
}
return instance;
}
@ -415,67 +392,37 @@ export class TerminalTab extends Disposable implements ITerminalTab {
}
public layout(width: number, height: number): void {
this._rootSplitPane.layoutBox(width, height);
if (this._splitPaneContainer) {
// Check if the panel position changed and rotate panes if so
const newPanelPosition = this._partService.getPanelPosition();
if (newPanelPosition !== this._panelPosition) {
const newOrientation = newPanelPosition === Position.BOTTOM ? Orientation.HORIZONTAL : Orientation.VERTICAL;
this._splitPaneContainer.setOrientation(newOrientation);
}
this._panelPosition = newPanelPosition;
this._splitPaneContainer.layout(width, height);
}
}
public focusDirection(direction: Direction): void {
const activeInstance = this.activeInstance;
if (!activeInstance) {
return null;
}
public focusPreviousPane(): void {
this.setActiveInstanceByIndex(this._activeInstanceIndex - 1);
}
const desiredOrientation = directionOrientation[direction];
const isUpOrLeft = direction === Direction.Left || direction === Direction.Up;
public focusNextPane(): void {
this.setActiveInstanceByIndex(this._activeInstanceIndex + 1);
}
// Find the closest horizontal SplitPane ancestor with a child to the left
let closestHorizontalPane: SplitPane = null;
const panePath = this._findSplitPanePath(activeInstance);
let index = panePath.length - 1;
let ancestorIndex: number;
while (--index >= 0) {
const pane = panePath[index];
// Continue up the path if not the desired orientation
if (pane.orientation !== desiredOrientation) {
continue;
}
// Find index of the panePath pane and break out of loop if it's not the left-most child
ancestorIndex = pane.children.indexOf(panePath[index + 1]);
// Make sure that the pane is not on the boundary
if (isUpOrLeft) {
if (ancestorIndex > 0) {
closestHorizontalPane = pane;
break;
}
} else {
if (ancestorIndex < pane.children.length - 1) {
closestHorizontalPane = pane;
break;
}
}
}
// There are no panes to the left
if (!closestHorizontalPane) {
public resizePane(direction: Direction): void {
if (!this._splitPaneContainer) {
return;
}
let current: SplitPane;
if (isUpOrLeft) {
// Find the bottom/right-most instance
current = closestHorizontalPane.children[ancestorIndex - 1];
while (current.children && current.children.length > 0) {
current = current.children[current.children.length - 1];
}
} else {
// Find the top/left-most instance
current = closestHorizontalPane.children[ancestorIndex + 1];
while (current.children && current.children.length > 0) {
current = current.children[0];
}
const isHorizontal = (direction === Direction.Left || direction === Direction.Right);
const font = this._terminalService.configHelper.getFont();
const amount = isHorizontal ? font.charWidth : font.charHeight;
if (amount) {
this._splitPaneContainer.resizePane(this._activeInstanceIndex, direction, amount);
}
// Focus the instance to the left
current.instance.focus();
}
}