Merge pull request #127120 from microsoft/tyriar/126256

Terminal editor + button
This commit is contained in:
Daniel Imms 2021-06-25 08:35:11 -07:00 committed by GitHub
commit 3c1fc33b75
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 365 additions and 120 deletions

View file

@ -32,8 +32,8 @@ export class DropdownWithPrimaryActionViewItem extends BaseActionViewItem {
dropdownMenuActions: IAction[],
className: string,
private readonly _contextMenuProvider: IContextMenuProvider,
_keybindingService: IKeybindingService,
_notificationService: INotificationService
@IKeybindingService _keybindingService: IKeybindingService,
@INotificationService _notificationService: INotificationService
) {
super(null, primaryAction);
this._primaryAction = new MenuEntryActionViewItem(primaryAction, _keybindingService, _notificationService);

View file

@ -594,7 +594,7 @@ export interface IBaseUnresolvedTerminalProfile {
args?: string | string[] | undefined;
isAutoDetected?: boolean;
overrideName?: boolean;
icon?: ThemeIcon | URI | { light: URI, dark: URI };
icon?: string | ThemeIcon | URI | { light: URI, dark: URI };
color?: string;
env?: ITerminalEnvironment;
}

View file

@ -9,7 +9,7 @@ import { findExecutable, getWindowsBuildNumber } from 'vs/platform/terminal/node
import * as cp from 'child_process';
import { ILogService } from 'vs/platform/log/common/log';
import * as pfs from 'vs/base/node/pfs';
import { ITerminalEnvironment, ITerminalProfile, ITerminalProfileObject, ProfileSource, TerminalSettingId } from 'vs/platform/terminal/common/terminal';
import { ITerminalEnvironment, ITerminalProfile, ITerminalProfileObject, ProfileSource, TerminalIcon, TerminalSettingId } from 'vs/platform/terminal/common/terminal';
import { Codicon } from 'vs/base/common/codicons';
import { isLinux, isWindows } from 'vs/base/common/platform';
import { ThemeIcon } from 'vs/platform/theme/common/themeService';
@ -156,14 +156,14 @@ async function transformToTerminalProfiles(
// if there are configured args, override the default ones
args = profile.args || source.args;
if (profile.icon) {
icon = profile.icon;
icon = validateIcon(profile.icon);
} else if (source.icon) {
icon = source.icon;
}
} else {
originalPaths = Array.isArray(profile.path) ? profile.path : [profile.path];
args = isWindows ? profile.args : Array.isArray(profile.args) ? profile.args : undefined;
icon = profile.icon || undefined;
icon = validateIcon(profile.icon) || undefined;
}
const paths = (await variableResolver?.(originalPaths)) || originalPaths.slice();
@ -180,6 +180,13 @@ async function transformToTerminalProfiles(
return resultProfiles;
}
function validateIcon(icon: string | TerminalIcon | undefined): TerminalIcon | undefined {
if (typeof icon === 'string') {
return { id: icon };
}
return icon;
}
async function initializeWindowsProfiles(testPwshSourcePaths?: string[]): Promise<void> {
if (profileSources && !testPwshSourcePaths) {
return;

View file

@ -153,6 +153,7 @@ export interface ITerminalService extends ITerminalInstanceHost {
/**
* Creates a raw terminal instance, this should not be used outside of the terminal part.
*/
createInstance(profile: ITerminalProfile): ITerminalInstance;
createInstance(shellLaunchConfig: IShellLaunchConfig): ITerminalInstance;
getInstanceFromId(terminalId: number): ITerminalInstance | undefined;
getInstanceFromIndex(terminalIndex: number): ITerminalInstance;
@ -197,7 +198,9 @@ export interface ITerminalService extends ITerminalInstanceHost {
setEditable(instance: ITerminalInstance, data: IEditableData | null): Promise<void>;
safeDisposeTerminal(instance: ITerminalInstance): Promise<void>;
getFindHost(): ITerminalFindHost;
getDefaultInstanceHost(): ITerminalInstanceHost;
getInstanceHost(target: TerminalLocation | undefined): ITerminalInstanceHost;
getFindHost(instance?: ITerminalInstance): ITerminalFindHost;
}
/**
@ -214,6 +217,7 @@ export interface ITerminalEditorService extends ITerminalInstanceHost, ITerminal
getOrCreateEditorInput(instance: ITerminalInstance | SerializedTerminalEditorInput): TerminalEditorInput;
detachActiveEditorInstance(): ITerminalInstance;
detachInstance(instance: ITerminalInstance): void;
splitInstance(instanceToSplit: ITerminalInstance, shellLaunchConfig?: IShellLaunchConfig): ITerminalInstance;
}
/**

View file

@ -30,7 +30,7 @@ import { ILocalTerminalService, ITerminalProfile, TerminalSettingId, TitleEventS
import { IWorkspaceContextService, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace';
import { PICK_WORKSPACE_FOLDER_COMMAND_ID } from 'vs/workbench/browser/actions/workspaceCommands';
import { FindInFilesCommand, IFindInFilesArgs } from 'vs/workbench/contrib/search/browser/searchActions';
import { Direction, IRemoteTerminalService, ITerminalEditorService, ITerminalGroupService, ITerminalInstance, ITerminalInstanceService, ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal';
import { Direction, ICreateTerminalOptions, IRemoteTerminalService, ITerminalGroupService, ITerminalInstance, ITerminalInstanceService, ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal';
import { TerminalQuickAccessProvider } from 'vs/workbench/contrib/terminal/browser/terminalQuickAccess';
import { IRemoteTerminalAttachTarget, ITerminalConfigHelper, KEYBINDING_CONTEXT_TERMINAL_A11Y_TREE_FOCUS, KEYBINDING_CONTEXT_TERMINAL_ALT_BUFFER_ACTIVE, KEYBINDING_CONTEXT_TERMINAL_FIND_FOCUSED, KEYBINDING_CONTEXT_TERMINAL_FIND_NOT_VISIBLE, KEYBINDING_CONTEXT_TERMINAL_FIND_VISIBLE, KEYBINDING_CONTEXT_TERMINAL_FOCUS, KEYBINDING_CONTEXT_TERMINAL_IS_OPEN, KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED, KEYBINDING_CONTEXT_TERMINAL_TABS_FOCUS, KEYBINDING_CONTEXT_TERMINAL_TABS_SINGULAR_SELECTION, KEYBINDING_CONTEXT_TERMINAL_TEXT_SELECTED, TerminalCommandId, TerminalLocation, TERMINAL_ACTION_CATEGORY } from 'vs/workbench/contrib/terminal/common/terminal';
import { terminalStrings } from 'vs/workbench/contrib/terminal/common/terminalStrings';
@ -141,22 +141,27 @@ export function registerTerminalActions() {
},
});
}
async run(accessor: ServicesAccessor, eventOrProfile: unknown | ITerminalProfile, profile?: ITerminalProfile) {
let event: MouseEvent | undefined;
if (eventOrProfile && typeof eventOrProfile === 'object' && 'profileName' in eventOrProfile) {
profile = eventOrProfile as ITerminalProfile;
} else {
event = eventOrProfile as MouseEvent;
}
async run(accessor: ServicesAccessor, eventOrOptionsOrProfile: MouseEvent | ICreateTerminalOptions | ITerminalProfile | undefined, profile?: ITerminalProfile) {
const terminalService = accessor.get(ITerminalService);
const terminalGroupService = accessor.get(ITerminalGroupService);
const workspaceContextService = accessor.get(IWorkspaceContextService);
const commandService = accessor.get(ICommandService);
let event: MouseEvent | PointerEvent | KeyboardEvent | undefined;
let options: ICreateTerminalOptions | undefined;
if (eventOrOptionsOrProfile instanceof MouseEvent || eventOrOptionsOrProfile instanceof PointerEvent || eventOrOptionsOrProfile instanceof KeyboardEvent) {
event = eventOrOptionsOrProfile;
options = profile ? { config: profile } : undefined;
} else {
options = convertOptionsOrProfileToOptions(eventOrOptionsOrProfile);
}
const folders = workspaceContextService.getWorkspace().folders;
if (event instanceof MouseEvent && (event.altKey || event.ctrlKey)) {
if (event && (event.altKey || event.ctrlKey)) {
const activeInstance = terminalService.activeInstance;
if (activeInstance) {
const cwd = await getCwdForSplit(terminalService.configHelper, activeInstance);
terminalService.splitInstance(activeInstance, profile, cwd);
terminalService.splitInstance(activeInstance, options?.config, cwd);
return;
}
}
@ -177,8 +182,8 @@ export function registerTerminalActions() {
cwd = workspace.uri;
}
if (profile) {
instance = terminalService.createTerminal({ config: profile, cwd });
if (options) {
instance = terminalService.createTerminal(options);
} else {
instance = await terminalService.showProfileQuickPick('createInstance', cwd);
}
@ -187,7 +192,7 @@ export function registerTerminalActions() {
terminalService.setActiveInstance(instance);
}
}
await accessor.get(ITerminalGroupService).showPanel(true);
await terminalGroupService.showPanel(true);
}
});
@ -299,8 +304,9 @@ export function registerTerminalActions() {
});
}
async run(accessor: ServicesAccessor) {
accessor.get(ITerminalGroupService).activeGroup?.focusPreviousPane();
await accessor.get(ITerminalGroupService).showPanel(true);
const terminalGroupService = accessor.get(ITerminalGroupService);
terminalGroupService.activeGroup?.focusPreviousPane();
await terminalGroupService.showPanel(true);
}
});
registerAction2(class extends Action2 {
@ -324,8 +330,9 @@ export function registerTerminalActions() {
});
}
async run(accessor: ServicesAccessor) {
accessor.get(ITerminalGroupService).activeGroup?.focusNextPane();
await accessor.get(ITerminalGroupService).showPanel(true);
const terminalGroupService = accessor.get(ITerminalGroupService);
terminalGroupService.activeGroup?.focusNextPane();
await terminalGroupService.showPanel(true);
}
});
registerAction2(class extends Action2 {
@ -464,8 +471,9 @@ export function registerTerminalActions() {
});
}
async run(accessor: ServicesAccessor) {
accessor.get(ITerminalGroupService).setActiveGroupToNext();
await accessor.get(ITerminalGroupService).showPanel(true);
const terminalGroupService = accessor.get(ITerminalGroupService);
terminalGroupService.setActiveGroupToNext();
await terminalGroupService.showPanel(true);
}
});
registerAction2(class extends Action2 {
@ -487,8 +495,9 @@ export function registerTerminalActions() {
});
}
async run(accessor: ServicesAccessor) {
accessor.get(ITerminalGroupService).setActiveGroupToPrevious();
await accessor.get(ITerminalGroupService).showPanel(true);
const terminalGroupService = accessor.get(ITerminalGroupService);
terminalGroupService.setActiveGroupToPrevious();
await terminalGroupService.showPanel(true);
}
});
registerAction2(class extends Action2 {
@ -503,6 +512,7 @@ export function registerTerminalActions() {
}
async run(accessor: ServicesAccessor) {
const terminalService = accessor.get(ITerminalService);
const terminalGroupService = accessor.get(ITerminalGroupService);
const codeEditorService = accessor.get(ICodeEditorService);
const instance = terminalService.getActiveOrCreateInstance();
@ -519,7 +529,7 @@ export function registerTerminalActions() {
text = editor.getModel().getValueInRange(selection, endOfLinePreference);
}
instance.sendText(text, true);
return accessor.get(ITerminalGroupService).showPanel();
return terminalGroupService.showPanel();
}
});
registerAction2(class extends Action2 {
@ -534,6 +544,8 @@ export function registerTerminalActions() {
}
async run(accessor: ServicesAccessor) {
const terminalService = accessor.get(ITerminalService);
const terminalGroupService = accessor.get(ITerminalGroupService);
const terminalInstanceService = accessor.get(ITerminalInstanceService);
const codeEditorService = accessor.get(ICodeEditorService);
const notificationService = accessor.get(INotificationService);
@ -550,9 +562,9 @@ export function registerTerminalActions() {
// TODO: Convert this to ctrl+c, ctrl+v for pwsh?
const instance = terminalService.getActiveOrCreateInstance();
const path = await accessor.get(ITerminalInstanceService).preparePathForTerminalAsync(uri.fsPath, instance.shellLaunchConfig.executable, instance.title, instance.shellType, instance.isRemote);
const path = await terminalInstanceService.preparePathForTerminalAsync(uri.fsPath, instance.shellLaunchConfig.executable, instance.title, instance.shellType, instance.isRemote);
instance.sendText(path, true);
return accessor.get(ITerminalGroupService).showPanel();
return terminalGroupService.showPanel();
}
});
registerAction2(class extends Action2 {
@ -1246,13 +1258,10 @@ export function registerTerminalActions() {
});
}
run(accessor: ServicesAccessor) {
if (accessor.get(ITerminalService).activeInstance?.target === TerminalLocation.Editor) {
const state = accessor.get(ITerminalEditorService).getFindState();
state.change({ matchCase: !state.matchCase }, false);
} else {
const state = accessor.get(ITerminalGroupService).getFindState();
state.change({ matchCase: !state.matchCase }, false);
}
const terminalService = accessor.get(ITerminalService);
const instanceHost = terminalService.getFindHost();
const state = instanceHost.getFindState();
state.change({ matchCase: !state.matchCase }, false);
}
});
registerAction2(class extends Action2 {
@ -1272,13 +1281,10 @@ export function registerTerminalActions() {
});
}
run(accessor: ServicesAccessor) {
if (accessor.get(ITerminalService).activeInstance?.target === TerminalLocation.Editor) {
const state = accessor.get(ITerminalEditorService).getFindState();
state.change({ wholeWord: !state.wholeWord }, false);
} else {
const state = accessor.get(ITerminalGroupService).getFindState();
state.change({ wholeWord: !state.wholeWord }, false);
}
const terminalService = accessor.get(ITerminalService);
const instanceHost = terminalService.getFindHost();
const state = instanceHost.getFindState();
state.change({ wholeWord: !state.wholeWord }, false);
}
});
registerAction2(class extends Action2 {
@ -1298,13 +1304,10 @@ export function registerTerminalActions() {
});
}
run(accessor: ServicesAccessor) {
if (accessor.get(ITerminalService).activeInstance?.target === TerminalLocation.Editor) {
const state = accessor.get(ITerminalEditorService).getFindState();
state.change({ matchCase: !state.matchCase }, false);
} else {
const state = accessor.get(ITerminalGroupService).getFindState();
state.change({ matchCase: !state.matchCase }, false);
}
const terminalService = accessor.get(ITerminalService);
const instanceHost = terminalService.getFindHost();
const state = instanceHost.getFindState();
state.change({ matchCase: !state.matchCase }, false);
}
});
registerAction2(class extends Action2 {
@ -1440,21 +1443,24 @@ export function registerTerminalActions() {
}
});
}
async run(accessor: ServicesAccessor, profile?: ITerminalProfile) {
const terminalService = accessor.get(ITerminalService);
async run(accessor: ServicesAccessor, optionsOrProfile?: ICreateTerminalOptions | ITerminalProfile) {
const commandService = accessor.get(ICommandService);
await terminalService.doWithActiveInstance(async t => {
const cwd = await getCwdForSplit(terminalService.configHelper, t, accessor.get(IWorkspaceContextService).getWorkspace().folders, accessor.get(ICommandService));
if (cwd === undefined) {
return undefined;
}
if (t.target === TerminalLocation.Editor) {
commandService.executeCommand('workbench.action.splitEditor');
} else {
terminalService.splitInstance(t, profile, cwd);
return accessor.get(ITerminalGroupService).showPanel(true);
}
});
const terminalGroupService = accessor.get(ITerminalGroupService);
const terminalService = accessor.get(ITerminalService);
const workspaceContextService = accessor.get(IWorkspaceContextService);
const options = convertOptionsOrProfileToOptions(optionsOrProfile);
const activeInstance = terminalService.getInstanceHost(options?.target).activeInstance;
if (!activeInstance) {
return;
}
const cwd = await getCwdForSplit(terminalService.configHelper, activeInstance, workspaceContextService.getWorkspace().folders, commandService);
if (cwd === undefined) {
return undefined;
}
const instance = terminalService.splitInstance(activeInstance, options?.config, cwd);
if (instance?.target !== TerminalLocation.Editor) {
return terminalGroupService.showPanel(true);
}
}
});
registerAction2(class extends Action2 {
@ -1478,6 +1484,7 @@ export function registerTerminalActions() {
}
async run(accessor: ServicesAccessor) {
const terminalService = accessor.get(ITerminalService);
const terminalGroupService = accessor.get(ITerminalGroupService);
const instances = getSelectedInstances(accessor);
if (instances) {
for (const t of instances) {
@ -1485,7 +1492,7 @@ export function registerTerminalActions() {
terminalService.doWithActiveInstance(async instance => {
const cwd = await getCwdForSplit(terminalService.configHelper, instance);
terminalService.splitInstance(instance, { cwd });
await accessor.get(ITerminalGroupService).showPanel(true);
await terminalGroupService.showPanel(true);
});
}
}
@ -1556,14 +1563,12 @@ export function registerTerminalActions() {
}
async run(accessor: ServicesAccessor) {
const terminalService = accessor.get(ITerminalService);
const commandService = accessor.get(ICommandService);
const terminalGroupService = accessor.get(ITerminalGroupService);
await terminalService.doWithActiveInstance(async t => {
if (t.target === TerminalLocation.Editor) {
commandService.executeCommand('workbench.action.splitEditor');
} else {
const cwd = await getCwdForSplit(terminalService.configHelper, t);
terminalService.splitInstance(t, { cwd });
await accessor.get(ITerminalGroupService).showPanel(true);
const cwd = await getCwdForSplit(terminalService.configHelper, t);
const instance = terminalService.splitInstance(t, { cwd });
if (instance?.target !== TerminalLocation.Editor) {
await terminalGroupService.showPanel(true);
}
});
}
@ -1611,6 +1616,7 @@ export function registerTerminalActions() {
}
async run(accessor: ServicesAccessor, event: unknown) {
const terminalService = accessor.get(ITerminalService);
const terminalGroupService = accessor.get(ITerminalGroupService);
const workspaceContextService = accessor.get(IWorkspaceContextService);
const commandService = accessor.get(ICommandService);
const folders = workspaceContextService.getWorkspace().folders;
@ -1646,7 +1652,7 @@ export function registerTerminalActions() {
}
terminalService.setActiveInstance(instance);
}
await accessor.get(ITerminalGroupService).showPanel(true);
await terminalGroupService.showPanel(true);
}
});
registerAction2(class extends Action2 {
@ -1661,16 +1667,15 @@ export function registerTerminalActions() {
});
}
async run(accessor: ServicesAccessor) {
const terminalService = accessor.get(ITerminalService);
await terminalService.doWithActiveInstance(async t => {
if (t.target === TerminalLocation.Editor) {
return;
}
t.dispose(true);
if (terminalService.instances.length > 0) {
await accessor.get(ITerminalGroupService).showPanel(true);
}
});
const terminalGroupService = accessor.get(ITerminalGroupService);
const instance = terminalGroupService.activeInstance;
if (!instance) {
return;
}
instance.dispose(true);
if (terminalGroupService.instances.length > 0) {
await terminalGroupService.showPanel(true);
}
}
});
@ -1859,6 +1864,7 @@ export function registerTerminalActions() {
}
async run(accessor: ServicesAccessor, item?: string) {
const terminalService = accessor.get(ITerminalService);
const terminalGroupService = accessor.get(ITerminalGroupService);
if (!item || !item.split) {
return Promise.resolve(null);
}
@ -1872,8 +1878,8 @@ export function registerTerminalActions() {
}
const indexMatches = terminalIndexRe.exec(item);
if (indexMatches) {
accessor.get(ITerminalGroupService).setActiveGroupByIndex(Number(indexMatches[1]) - 1);
return accessor.get(ITerminalGroupService).showPanel(true);
terminalGroupService.setActiveGroupByIndex(Number(indexMatches[1]) - 1);
return terminalGroupService.showPanel(true);
}
const quickSelectProfiles = terminalService.availableProfiles;
@ -1942,3 +1948,10 @@ export function validateTerminalName(name: string): { content: string, severity:
return null;
}
function convertOptionsOrProfileToOptions(optionsOrProfile?: ICreateTerminalOptions | ITerminalProfile): ICreateTerminalOptions | undefined {
if (typeof optionsOrProfile === 'object' && 'profileName' in optionsOrProfile) {
return { config: optionsOrProfile as ITerminalProfile };
}
return optionsOrProfile;
}

View file

@ -4,20 +4,30 @@
*--------------------------------------------------------------------------------------------*/
import { Dimension } from 'vs/base/browser/dom';
import { IActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar';
import { Action, IAction, Separator, SubmenuAction } from 'vs/base/common/actions';
import { CancellationToken } from 'vs/base/common/cancellation';
import { Codicon } from 'vs/base/common/codicons';
import { FindReplaceState } from 'vs/editor/contrib/find/findState';
import { localize } from 'vs/nls';
import { DropdownWithPrimaryActionViewItem } from 'vs/platform/actions/browser/dropdownWithPrimaryActionViewItem';
import { IMenu, IMenuActionOptions, IMenuService, MenuId, MenuItemAction } from 'vs/platform/actions/common/actions';
import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { IEditorOptions } from 'vs/platform/editor/common/editor';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { ITerminalProfile } from 'vs/platform/terminal/common/terminal';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane';
import { IEditorOpenContext } from 'vs/workbench/common/editor';
import { ITerminalEditorService } from 'vs/workbench/contrib/terminal/browser/terminal';
import { ICreateTerminalOptions, ITerminalEditorService, ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal';
import { TerminalEditorInput } from 'vs/workbench/contrib/terminal/browser/terminalEditorInput';
import { TerminalFindWidget } from 'vs/workbench/contrib/terminal/browser/terminalFindWidget';
import { KEYBINDING_CONTEXT_TERMINAL_FIND_VISIBLE } from 'vs/workbench/contrib/terminal/common/terminal';
import { TerminalTabContextMenuGroup } from 'vs/workbench/contrib/terminal/browser/terminalMenus';
import { ITerminalProfileResolverService, KEYBINDING_CONTEXT_TERMINAL_FIND_VISIBLE, TerminalCommandId, TerminalLocation } from 'vs/workbench/contrib/terminal/common/terminal';
import { terminalStrings } from 'vs/workbench/contrib/terminal/common/terminalStrings';
import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService';
const xtermSelector = '.terminal.xterm';
@ -33,6 +43,8 @@ export class TerminalEditor extends EditorPane {
private _lastDimension?: Dimension;
private readonly _dropdownMenu: IMenu;
private _findWidget: TerminalFindWidget;
private _findWidgetVisible: IContextKey<boolean>;
private _findState: FindReplaceState;
@ -44,13 +56,20 @@ export class TerminalEditor extends EditorPane {
@IThemeService themeService: IThemeService,
@IStorageService storageService: IStorageService,
@ITerminalEditorService private readonly _terminalEditorService: ITerminalEditorService,
@ITerminalProfileResolverService private readonly _terminalProfileResolverService: ITerminalProfileResolverService,
// @ITerminalContributionService private readonly _terminalContributionService: ITerminalContributionService,
@ITerminalService private readonly _terminalService: ITerminalService,
@IInstantiationService instantiationService: IInstantiationService,
@IContextKeyService contextKeyService: IContextKeyService
@IContextKeyService contextKeyService: IContextKeyService,
@IMenuService menuService: IMenuService,
@IInstantiationService private readonly _instantiationService: IInstantiationService,
@IContextMenuService private readonly _contextMenuService: IContextMenuService,
) {
super(TerminalEditor.ID, telemetryService, themeService, storageService);
this._findState = new FindReplaceState();
this._findWidget = instantiationService.createInstance(TerminalFindWidget, this._findState);
this._findWidgetVisible = KEYBINDING_CONTEXT_TERMINAL_FIND_VISIBLE.bindTo(contextKeyService);
this._dropdownMenu = this._register(menuService.createMenu(MenuId.TerminalNewDropdownContext, contextKeyService));
}
override async setInput(newInput: TerminalEditorInput, options: IEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken) {
@ -96,6 +115,84 @@ export class TerminalEditor extends EditorPane {
return this._editorInput?.terminalInstance?.setVisible(visible);
}
override getActionViewItem(action: IAction): IActionViewItem | undefined {
switch (action.id) {
case TerminalCommandId.CreateWithProfileButton: {
const actions = this._getTabActionBarArgs(this._terminalService.availableProfiles);
const button = this._instantiationService.createInstance(DropdownWithPrimaryActionViewItem, actions.primaryAction, actions.dropdownAction, actions.dropdownMenuActions, actions.className, this._contextMenuService);
return button;
}
}
return super.getActionViewItem(action);
}
private _getTabActionBarArgs(profiles: ITerminalProfile[]): {
primaryAction: MenuItemAction,
dropdownAction: IAction,
dropdownMenuActions: IAction[],
className: string,
dropdownIcon?: string
} {
const dropdownActions: IAction[] = [];
const submenuActions: IAction[] = [];
const defaultProfileName = this._terminalProfileResolverService.defaultProfileName;
for (const p of profiles) {
const isDefault = p.profileName === defaultProfileName;
const options: IMenuActionOptions = {
arg: {
config: p,
target: TerminalLocation.Editor
} as ICreateTerminalOptions,
shouldForwardArgs: true
};
if (isDefault) {
dropdownActions.unshift(this._instantiationService.createInstance(MenuItemAction, { id: TerminalCommandId.NewWithProfile, title: localize('defaultTerminalProfile', "{0} (Default)", p.profileName), category: TerminalTabContextMenuGroup.Profile }, undefined, options));
submenuActions.unshift(this._instantiationService.createInstance(MenuItemAction, { id: TerminalCommandId.Split, title: localize('defaultTerminalProfile', "{0} (Default)", p.profileName), category: TerminalTabContextMenuGroup.Profile }, undefined, options));
} else {
dropdownActions.push(this._instantiationService.createInstance(MenuItemAction, { id: TerminalCommandId.NewWithProfile, title: p.profileName.replace(/[\n\r\t]/g, ''), category: TerminalTabContextMenuGroup.Profile }, undefined, options));
submenuActions.push(this._instantiationService.createInstance(MenuItemAction, { id: TerminalCommandId.Split, title: p.profileName.replace(/[\n\r\t]/g, ''), category: TerminalTabContextMenuGroup.Profile }, undefined, options));
}
}
// TODO: Support contributed profiles with editor target
// for (const contributed of this._terminalContributionService.terminalProfiles) {
// dropdownActions.push(new Action(TerminalCommandId.NewWithProfile, contributed.title.replace(/[\n\r\t]/g, ''), undefined, true, () => this._terminalService.createContributedTerminalProfile(contributed.extensionIdentifier, contributed.id, false)));
// submenuActions.push(new Action(TerminalCommandId.NewWithProfile, contributed.title.replace(/[\n\r\t]/g, ''), undefined, true, () => this._terminalService.createContributedTerminalProfile(contributed.extensionIdentifier, contributed.id, true)));
// }
if (dropdownActions.length > 0) {
dropdownActions.push(new SubmenuAction('split.profile', 'Split...', submenuActions));
dropdownActions.push(new Separator());
}
for (const [, configureActions] of this._dropdownMenu.getActions()) {
for (const action of configureActions) {
// make sure the action is a MenuItemAction
if ('alt' in action) {
dropdownActions.push(action);
}
}
}
const primaryAction = this._instantiationService.createInstance(
MenuItemAction,
{
id: TerminalCommandId.CreateTerminalEditor,
title: localize('terminal.new', "New Terminal"),
icon: Codicon.plus
},
{
id: 'workbench.action.splitEditor',
title: terminalStrings.split.value,
icon: Codicon.splitHorizontal
},
undefined);
const dropdownAction = new Action('refresh profiles', 'Launch Profile...', 'codicon-chevron-down', true);
return { primaryAction, dropdownAction, dropdownMenuActions: dropdownActions, className: 'terminal-tab-actions' };
}
focusFindWidget() {
if (this._parentElement && !this._parentElement?.querySelector(findWidgetSelector)) {
this._parentElement.querySelector(xtermSelector)!.appendChild(this._findWidget.getDomNode());

View file

@ -19,6 +19,7 @@ export class TerminalEditorInput extends EditorInput {
static readonly ID = 'workbench.editors.terminal';
private _isDetached = false;
private _copyInstance?: ITerminalInstance;
override get typeId(): string {
return TerminalEditorInput.ID;
@ -29,10 +30,19 @@ export class TerminalEditorInput extends EditorInput {
}
override copy(): IEditorInput {
const instance = this._terminalInstanceService.createInstance({}, TerminalLocation.Editor);
const instance = this._copyInstance || this._terminalInstanceService.createInstance({}, TerminalLocation.Editor);
this._copyInstance = undefined;
return this._instantiationService.createInstance(TerminalEditorInput, instance);
}
/**
* Sets what instance to use for the next call to IEditorInput.copy, this is used to define what
* terminal instance is used when the editor's split command is run.
*/
setCopyInstance(instance: ITerminalInstance) {
this._copyInstance = instance;
}
/**
* Returns the terminal instance for this input if it has not yet been detached from the input.
*/
@ -48,7 +58,7 @@ export class TerminalEditorInput extends EditorInput {
private readonly _terminalInstance: ITerminalInstance,
@IThemeService private readonly _themeService: IThemeService,
@ITerminalInstanceService private readonly _terminalInstanceService: ITerminalInstanceService,
private readonly _instantiationService: IInstantiationService
@IInstantiationService private readonly _instantiationService: IInstantiationService
) {
super();
this._register(this._terminalInstance.onTitleChanged(() => this._onDidChangeLabel.fire()));

View file

@ -6,16 +6,18 @@
import { Emitter, Event } from 'vs/base/common/event';
import { Disposable, dispose, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { FindReplaceState } from 'vs/editor/contrib/find/findState';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IShellLaunchConfig } from 'vs/platform/terminal/common/terminal';
import { IEditorInput } from 'vs/workbench/common/editor';
import { ITerminalEditorService, ITerminalFindHost, ITerminalInstance, ITerminalInstanceService } from 'vs/workbench/contrib/terminal/browser/terminal';
import { ITerminalEditorService, ITerminalInstance, ITerminalInstanceService } from 'vs/workbench/contrib/terminal/browser/terminal';
import { TerminalEditor } from 'vs/workbench/contrib/terminal/browser/terminalEditor';
import { TerminalEditorInput } from 'vs/workbench/contrib/terminal/browser/terminalEditorInput';
import { SerializedTerminalEditorInput } from 'vs/workbench/contrib/terminal/browser/terminalEditorSerializer';
import { TerminalLocation } from 'vs/workbench/contrib/terminal/common/terminal';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
export class TerminalEditorService extends Disposable implements ITerminalEditorService, ITerminalFindHost {
export class TerminalEditorService extends Disposable implements ITerminalEditorService {
declare _serviceBrand: undefined;
instances: ITerminalInstance[] = [];
@ -32,6 +34,7 @@ export class TerminalEditorService extends Disposable implements ITerminalEditor
get onDidChangeInstances(): Event<void> { return this._onDidChangeInstances.event; }
constructor(
@ICommandService private readonly _commandService: ICommandService,
@IEditorService private readonly _editorService: IEditorService,
@ITerminalInstanceService private readonly _terminalInstanceService: ITerminalInstanceService,
@IInstantiationService private readonly _instantiationService: IInstantiationService
@ -125,7 +128,7 @@ export class TerminalEditorService extends Disposable implements ITerminalEditor
});
}
getOrCreateEditorInput(instance: ITerminalInstance | SerializedTerminalEditorInput): TerminalEditorInput {
getOrCreateEditorInput(instance: ITerminalInstance | SerializedTerminalEditorInput, isFutureSplit: boolean = false): TerminalEditorInput {
let cachedEditor;
if ('id' in instance) {
cachedEditor = this._editorInputs.get(instance.id);
@ -151,6 +154,14 @@ export class TerminalEditorService extends Disposable implements ITerminalEditor
return input;
}
splitInstance(instanceToSplit: ITerminalInstance, shellLaunchConfig: IShellLaunchConfig = {}): ITerminalInstance {
const input = this.getOrCreateEditorInput(instanceToSplit);
const instance = this._terminalInstanceService.createInstance(shellLaunchConfig, TerminalLocation.Editor);
input.setCopyInstance(instance);
this._commandService.executeCommand('workbench.action.splitEditor');
return instance;
}
detachActiveEditorInstance(): ITerminalInstance {
const activeEditor = this._editorService.activeEditor;
if (!(activeEditor instanceof TerminalEditorInput)) {

View file

@ -10,7 +10,7 @@ import type { Unicode11Addon as XTermUnicode11Addon } from 'xterm-addon-unicode1
import type { WebglAddon as XTermWebglAddon } from 'xterm-addon-webgl';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { Disposable } from 'vs/base/common/lifecycle';
import { ILocalTerminalService, IShellLaunchConfig, TerminalShellType, WindowsShellType } from 'vs/platform/terminal/common/terminal';
import { ILocalTerminalService, IShellLaunchConfig, ITerminalProfile, TerminalShellType, WindowsShellType } from 'vs/platform/terminal/common/terminal';
import { IInstantiationService, optional } from 'vs/platform/instantiation/common/instantiation';
import { escapeNonWindowsPath } from 'vs/platform/terminal/common/terminalEnvironment';
import { basename } from 'vs/base/common/path';
@ -19,6 +19,7 @@ import { TerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminal
import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { TerminalConfigHelper } from 'vs/workbench/contrib/terminal/browser/terminalConfigHelper';
import { KEYBINDING_CONTEXT_TERMINAL_FOCUS, KEYBINDING_CONTEXT_TERMINAL_SHELL_TYPE, KEYBINDING_CONTEXT_TERMINAL_ALT_BUFFER_ACTIVE, TerminalLocation } from 'vs/workbench/contrib/terminal/common/terminal';
import { URI } from 'vs/base/common/uri';
let Terminal: typeof XTermTerminal;
let SearchAddon: typeof XTermSearchAddon;
@ -47,20 +48,48 @@ export class TerminalInstanceService extends Disposable implements ITerminalInst
this._configHelper = _instantiationService.createInstance(TerminalConfigHelper);
}
createInstance(launchConfig: IShellLaunchConfig, target?: TerminalLocation): ITerminalInstance {
createInstance(profile: ITerminalProfile, target?: TerminalLocation): ITerminalInstance;
createInstance(shellLaunchConfig: IShellLaunchConfig, target?: TerminalLocation): ITerminalInstance;
createInstance(config: IShellLaunchConfig | ITerminalProfile, target?: TerminalLocation): ITerminalInstance {
const shellLaunchConfig = this._convertProfileToShellLaunchConfig(config);
const instance = this._instantiationService.createInstance(TerminalInstance,
this._terminalFocusContextKey,
this._terminalShellTypeContextKey,
this._terminalAltBufferActiveContextKey,
this._configHelper,
launchConfig
shellLaunchConfig
);
if (target) {
instance.target = TerminalLocation.Editor;
}
instance.target = target;
return instance;
}
private _convertProfileToShellLaunchConfig(shellLaunchConfigOrProfile?: IShellLaunchConfig | ITerminalProfile, cwd?: string | URI): IShellLaunchConfig {
// Profile was provided
if (shellLaunchConfigOrProfile && 'profileName' in shellLaunchConfigOrProfile) {
const profile = shellLaunchConfigOrProfile;
return {
executable: profile.path,
args: profile.args,
env: profile.env,
icon: profile.icon,
color: profile.color,
name: profile.overrideName ? profile.profileName : undefined,
cwd
};
}
// Shell launch config was provided
if (shellLaunchConfigOrProfile) {
if (cwd) {
shellLaunchConfigOrProfile.cwd = cwd;
}
return shellLaunchConfigOrProfile;
}
// Return empty shell launch config
return {};
}
async getXtermConstructor(): Promise<typeof XTermTerminal> {
if (!Terminal) {
Terminal = (await import('xterm')).Terminal;

View file

@ -529,4 +529,14 @@ export function setupTerminalMenus(): void {
when: ResourceContextKey.Scheme.isEqualTo(Schemas.vscodeTerminal),
group: '2_files'
});
MenuRegistry.appendMenuItem(MenuId.EditorTitle, {
command: {
id: TerminalCommandId.CreateWithProfileButton,
title: TerminalCommandId.CreateWithProfileButton
},
group: 'navigation',
order: 0,
when: ResourceContextKey.Scheme.isEqualTo(Schemas.vscodeTerminal)
});
}

View file

@ -597,10 +597,6 @@ export class TerminalService implements ITerminalService {
splitInstance(instanceToSplit: ITerminalInstance, shellLaunchConfig?: IShellLaunchConfig): ITerminalInstance | null;
splitInstance(instanceToSplit: ITerminalInstance, profile: ITerminalProfile, cwd?: string | URI): ITerminalInstance | null
splitInstance(instanceToSplit: ITerminalInstance, shellLaunchConfigOrProfile: IShellLaunchConfig | ITerminalProfile = {}, cwd?: string | URI): ITerminalInstance | null {
const group = this._terminalGroupService.getGroupForInstance(instanceToSplit);
if (!group) {
return null;
}
const shellLaunchConfig = this._convertProfileToShellLaunchConfig(shellLaunchConfigOrProfile, cwd);
// Use the URI from the base instance if it exists, this will correctly split local terminals
@ -612,11 +608,27 @@ export class TerminalService implements ITerminalService {
this._evaluateLocalCwd(shellLaunchConfig);
}
const instance = group.split(shellLaunchConfig);
// Handle editor terminals
let instance: ITerminalInstance;
switch (instanceToSplit.target) {
case TerminalLocation.Editor:
instance = this._terminalEditorService.splitInstance(instanceToSplit, shellLaunchConfig);
break;
case TerminalLocation.TerminalView:
default:
const group = this._terminalGroupService.getGroupForInstance(instanceToSplit);
if (!group) {
return null;
}
instance = group.split(shellLaunchConfig);
break;
}
this._initInstanceListeners(instance);
this._terminalGroupService.groups.forEach((g, i) => g.setVisible(i === this._terminalGroupService.activeGroupIndex));
if (instanceToSplit.target !== TerminalLocation.Editor) {
this._terminalGroupService.groups.forEach((g, i) => g.setVisible(i === this._terminalGroupService.activeGroupIndex));
}
return instance;
}
@ -865,7 +877,7 @@ export class TerminalService implements ITerminalService {
return;
}
if (type === 'createInstance') {
const activeInstance = this.activeInstance;
const activeInstance = this.getDefaultInstanceHost().activeInstance;
let instance;
if ('id' in value.profile) {
@ -910,8 +922,25 @@ export class TerminalService implements ITerminalService {
return undefined;
}
getFindHost(): ITerminalFindHost {
return this.activeInstance?.target === TerminalLocation.Editor ? this._terminalEditorService : this._terminalGroupService;
getDefaultInstanceHost(): ITerminalInstanceHost {
if (this.configHelper.config.defaultLocation === TerminalLocation.Editor) {
return this._terminalEditorService;
}
return this._terminalGroupService;
}
getInstanceHost(target: TerminalLocation | undefined): ITerminalInstanceHost {
if (target) {
if (target === TerminalLocation.Editor) {
return this._terminalEditorService;
}
return this._terminalGroupService;
}
return this;
}
getFindHost(instance: ITerminalInstance | undefined = this.activeInstance): ITerminalFindHost {
return instance?.target === TerminalLocation.Editor ? this._terminalEditorService : this._terminalGroupService;
}
async createContributedTerminalProfile(extensionIdentifier: string, id: string, isSplitTerminal: boolean): Promise<void> {

View file

@ -14,17 +14,17 @@ import { IThemeService, IColorTheme, registerThemingParticipant, ICssStyleCollec
import { switchTerminalActionViewItemSeparator, switchTerminalShowTabsTitle } from 'vs/workbench/contrib/terminal/browser/terminalActions';
import { TERMINAL_BACKGROUND_COLOR, TERMINAL_BORDER_COLOR, TERMINAL_DRAG_AND_DROP_BACKGROUND } from 'vs/workbench/contrib/terminal/common/terminalColorRegistry';
import { INotificationService, IPromptChoice, Severity } from 'vs/platform/notification/common/notification';
import { ITerminalGroupService, ITerminalInstance, ITerminalService, TerminalConnectionState } from 'vs/workbench/contrib/terminal/browser/terminal';
import { ICreateTerminalOptions, ITerminalGroupService, ITerminalInstance, ITerminalService, TerminalConnectionState } from 'vs/workbench/contrib/terminal/browser/terminal';
import { ViewPane, IViewPaneOptions } from 'vs/workbench/browser/parts/views/viewPane';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IViewDescriptorService } from 'vs/workbench/common/views';
import { IOpenerService } from 'vs/platform/opener/common/opener';
import { PANEL_BACKGROUND, SIDE_BAR_BACKGROUND, EDITOR_DRAG_AND_DROP_BACKGROUND } from 'vs/workbench/common/theme';
import { IMenu, IMenuService, MenuId, MenuItemAction } from 'vs/platform/actions/common/actions';
import { ITerminalProfileResolverService, TerminalCommandId } from 'vs/workbench/contrib/terminal/common/terminal';
import { IMenu, IMenuActionOptions, IMenuService, MenuId, MenuItemAction } from 'vs/platform/actions/common/actions';
import { ITerminalProfileResolverService, TerminalCommandId, TerminalLocation } from 'vs/workbench/contrib/terminal/common/terminal';
import { TerminalSettingId, ITerminalProfile } from 'vs/platform/terminal/common/terminal';
import { SelectActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems';
import { ActionViewItem, SelectActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems';
import { ITerminalContributionService } from 'vs/workbench/contrib/terminal/common/terminalExtensionPoints';
import { attachSelectBoxStyler, attachStylerCallback } from 'vs/platform/theme/common/styler';
import { selectBorder } from 'vs/platform/theme/common/colorRegistry';
@ -43,6 +43,7 @@ import { URI } from 'vs/base/common/uri';
import { ColorScheme } from 'vs/platform/theme/common/theme';
import { getColorClass, getUriClasses } from 'vs/workbench/contrib/terminal/browser/terminalIcon';
import { terminalStrings } from 'vs/workbench/contrib/terminal/common/terminalStrings';
import { withNullAsUndefined } from 'vs/base/common/types';
export class TerminalViewPane extends ViewPane {
private _actions: IAction[] | undefined;
@ -178,6 +179,26 @@ export class TerminalViewPane extends ViewPane {
override getActionViewItem(action: Action): IActionViewItem | undefined {
switch (action.id) {
case TerminalCommandId.Split: {
// Split needs to be special cased to force splitting within the panel, not the editor
const panelOnlySplitAction: IAction = {
id: action.id,
checked: action.checked,
class: action.class,
enabled: action.enabled,
label: action.label,
dispose: action.dispose.bind(action),
tooltip: action.tooltip,
run: () => {
const instance = this._terminalGroupService.activeInstance;
if (instance) {
return this._terminalService.splitInstance(instance);
}
return;
}
};
return new ActionViewItem(action, panelOnlySplitAction, { icon: true, label: false, keybinding: this._getKeybindingLabel(action) });
}
case TerminalCommandId.SwitchTerminal: {
return this._instantiationService.createInstance(SwitchTerminalActionViewItem, action);
}
@ -200,6 +221,10 @@ export class TerminalViewPane extends ViewPane {
return super.getActionViewItem(action);
}
private _getKeybindingLabel(action: IAction): string | undefined {
return withNullAsUndefined(this._keybindingService.lookupKeybinding(action.id)?.getLabel());
}
private _updateTabActionBar(profiles: ITerminalProfile[]): void {
const actions = this._getTabActionBarArgs(profiles);
this._tabButtons?.update(actions.dropdownAction, actions.dropdownMenuActions);
@ -218,12 +243,19 @@ export class TerminalViewPane extends ViewPane {
const defaultProfileName = this._terminalProfileResolverService.defaultProfileName;
for (const p of profiles) {
const isDefault = p.profileName === defaultProfileName;
const options: IMenuActionOptions = {
arg: {
config: p,
target: TerminalLocation.TerminalView
} as ICreateTerminalOptions,
shouldForwardArgs: true
};
if (isDefault) {
dropdownActions.unshift(new MenuItemAction({ id: TerminalCommandId.NewWithProfile, title: nls.localize('defaultTerminalProfile', "{0} (Default)", p.profileName), category: TerminalTabContextMenuGroup.Profile }, undefined, { arg: p, shouldForwardArgs: true }, this._contextKeyService, this._commandService));
submenuActions.unshift(new MenuItemAction({ id: TerminalCommandId.Split, title: nls.localize('defaultTerminalProfile', "{0} (Default)", p.profileName), category: TerminalTabContextMenuGroup.Profile }, undefined, { arg: p, shouldForwardArgs: true }, this._contextKeyService, this._commandService));
dropdownActions.unshift(new MenuItemAction({ id: TerminalCommandId.NewWithProfile, title: nls.localize('defaultTerminalProfile', "{0} (Default)", p.profileName), category: TerminalTabContextMenuGroup.Profile }, undefined, options, this._contextKeyService, this._commandService));
submenuActions.unshift(new MenuItemAction({ id: TerminalCommandId.Split, title: nls.localize('defaultTerminalProfile', "{0} (Default)", p.profileName), category: TerminalTabContextMenuGroup.Profile }, undefined, options, this._contextKeyService, this._commandService));
} else {
dropdownActions.push(new MenuItemAction({ id: TerminalCommandId.NewWithProfile, title: p.profileName.replace(/[\n\r\t]/g, ''), category: TerminalTabContextMenuGroup.Profile }, undefined, { arg: p, shouldForwardArgs: true }, this._contextKeyService, this._commandService));
submenuActions.push(new MenuItemAction({ id: TerminalCommandId.Split, title: p.profileName.replace(/[\n\r\t]/g, ''), category: TerminalTabContextMenuGroup.Profile }, undefined, { arg: p, shouldForwardArgs: true }, this._contextKeyService, this._commandService));
dropdownActions.push(new MenuItemAction({ id: TerminalCommandId.NewWithProfile, title: p.profileName.replace(/[\n\r\t]/g, ''), category: TerminalTabContextMenuGroup.Profile }, undefined, options, this._contextKeyService, this._commandService));
submenuActions.push(new MenuItemAction({ id: TerminalCommandId.Split, title: p.profileName.replace(/[\n\r\t]/g, ''), category: TerminalTabContextMenuGroup.Profile }, undefined, options, this._contextKeyService, this._commandService));
}
}
@ -258,7 +290,10 @@ export class TerminalViewPane extends ViewPane {
title: terminalStrings.split.value,
icon: Codicon.splitHorizontal
},
undefined);
{
shouldForwardArgs: true,
arg: { target: TerminalLocation.TerminalView } as ICreateTerminalOptions,
});
const dropdownAction = new Action('refresh profiles', 'Launch Profile...', 'codicon-chevron-down', true);
return { primaryAction, dropdownAction, dropdownMenuActions: dropdownActions, className: 'terminal-tab-actions' };
@ -410,7 +445,7 @@ class SingleTerminalTabActionViewItem extends MenuEntryActionViewItem {
override async onClick(event: MouseEvent): Promise<void> {
if (event.altKey && this._menuItemAction.alt) {
this._commandService.executeCommand(this._menuItemAction.alt.id);
this._commandService.executeCommand(this._menuItemAction.alt.id, { target: TerminalLocation.TerminalView } as ICreateTerminalOptions);
} else {
this._openContextMenu();
}