1150 lines
50 KiB
TypeScript
1150 lines
50 KiB
TypeScript
/*---------------------------------------------------------------------------------------------
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the MIT License. See License.txt in the project root for license information.
|
|
*--------------------------------------------------------------------------------------------*/
|
|
|
|
import * as dom from 'vs/base/browser/dom';
|
|
import { timeout } from 'vs/base/common/async';
|
|
import { Codicon, iconRegistry } from 'vs/base/common/codicons';
|
|
import { debounce } from 'vs/base/common/decorators';
|
|
import { Emitter, Event } from 'vs/base/common/event';
|
|
import { dispose, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
|
|
import { Schemas } from 'vs/base/common/network';
|
|
import { isMacintosh, isWeb } from 'vs/base/common/platform';
|
|
import { URI } from 'vs/base/common/uri';
|
|
import { FindReplaceState } from 'vs/editor/contrib/find/findState';
|
|
import * as nls from 'vs/nls';
|
|
import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
|
|
import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
|
|
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
|
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
|
import { ICreateContributedTerminalProfileOptions, IShellLaunchConfig, ITerminalLaunchError, ITerminalsLayoutInfo, ITerminalsLayoutInfoById, TerminalLocation, TerminalLocationString } from 'vs/platform/terminal/common/terminal';
|
|
import { iconForeground } from 'vs/platform/theme/common/colorRegistry';
|
|
import { IconDefinition } from 'vs/platform/theme/common/iconRegistry';
|
|
import { ColorScheme } from 'vs/platform/theme/common/theme';
|
|
import { IThemeService, Themable, ThemeIcon } from 'vs/platform/theme/common/themeService';
|
|
import { VirtualWorkspaceContext } from 'vs/workbench/browser/contextkeys';
|
|
import { IEditableData, IViewsService } from 'vs/workbench/common/views';
|
|
import { ICreateTerminalOptions, IRequestAddInstanceToGroupEvent, ITerminalEditorService, ITerminalExternalLinkProvider, ITerminalFindHost, ITerminalGroup, ITerminalGroupService, ITerminalInstance, ITerminalInstanceHost, ITerminalInstanceService, ITerminalLocationOptions, ITerminalService, ITerminalServiceNativeDelegate, TerminalConnectionState, TerminalEditorLocation } from 'vs/workbench/contrib/terminal/browser/terminal';
|
|
import { TerminalConfigHelper } from 'vs/workbench/contrib/terminal/browser/terminalConfigHelper';
|
|
import { getColorStyleContent, getUriClasses } from 'vs/workbench/contrib/terminal/browser/terminalIcon';
|
|
import { getInstanceFromResource, getTerminalUri, parseTerminalUri } from 'vs/workbench/contrib/terminal/browser/terminalUri';
|
|
import { TerminalViewPane } from 'vs/workbench/contrib/terminal/browser/terminalView';
|
|
import { IRemoteTerminalAttachTarget, IStartExtensionTerminalRequest, ITerminalConfigHelper, ITerminalBackend, ITerminalProcessExtHostProxy, ITerminalProfileService, TERMINAL_VIEW_ID } from 'vs/workbench/contrib/terminal/common/terminal';
|
|
import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey';
|
|
import { formatMessageForTerminal } from 'vs/workbench/contrib/terminal/common/terminalStrings';
|
|
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
|
|
import { ILifecycleService, ShutdownReason, WillShutdownEvent } from 'vs/workbench/services/lifecycle/common/lifecycle';
|
|
import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService';
|
|
import { ACTIVE_GROUP, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService';
|
|
import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService';
|
|
import { INotificationService } from 'vs/platform/notification/common/notification';
|
|
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
|
|
import { TerminalProfileQuickpick } from 'vs/workbench/contrib/terminal/browser/terminalProfileQuickpick';
|
|
import { IKeyMods } from 'vs/base/parts/quickinput/common/quickInput';
|
|
import { ILogService } from 'vs/platform/log/common/log';
|
|
|
|
export class TerminalService implements ITerminalService {
|
|
declare _serviceBrand: undefined;
|
|
|
|
private _hostActiveTerminals: Map<ITerminalInstanceHost, ITerminalInstance | undefined> = new Map();
|
|
|
|
private _isShuttingDown: boolean = false;
|
|
private _backgroundedTerminalInstances: ITerminalInstance[] = [];
|
|
private _backgroundedTerminalDisposables: Map<number, IDisposable[]> = new Map();
|
|
private _findState: FindReplaceState = new FindReplaceState();
|
|
private _linkProviders: Set<ITerminalExternalLinkProvider> = new Set();
|
|
private _linkProviderDisposables: Map<ITerminalExternalLinkProvider, IDisposable[]> = new Map();
|
|
private _processSupportContextKey: IContextKey<boolean>;
|
|
|
|
private _primaryBackend?: ITerminalBackend;
|
|
private _terminalHasBeenCreated: IContextKey<boolean>;
|
|
private _terminalCountContextKey: IContextKey<number>;
|
|
private _configHelper: TerminalConfigHelper;
|
|
private _remoteTerminalsInitPromise: Promise<void> | undefined;
|
|
private _localTerminalsInitPromise: Promise<void> | undefined;
|
|
private _connectionState: TerminalConnectionState = TerminalConnectionState.Connecting;
|
|
private _nativeDelegate?: ITerminalServiceNativeDelegate;
|
|
private _shutdownWindowCount?: number;
|
|
|
|
private _editable: { instance: ITerminalInstance, data: IEditableData } | undefined;
|
|
|
|
get isProcessSupportRegistered(): boolean { return !!this._processSupportContextKey.get(); }
|
|
get connectionState(): TerminalConnectionState { return this._connectionState; }
|
|
|
|
get configHelper(): ITerminalConfigHelper { return this._configHelper; }
|
|
get instances(): ITerminalInstance[] {
|
|
return this._terminalGroupService.instances.concat(this._terminalEditorService.instances);
|
|
}
|
|
|
|
get defaultLocation(): TerminalLocation { return this.configHelper.config.defaultLocation === TerminalLocationString.Editor ? TerminalLocation.Editor : TerminalLocation.Panel; }
|
|
|
|
private _activeInstance: ITerminalInstance | undefined;
|
|
get activeInstance(): ITerminalInstance | undefined {
|
|
// Check if either an editor or panel terminal has focus and return that, regardless of the
|
|
// value of _activeInstance. This avoids terminals created in the panel for example stealing
|
|
// the active status even when it's not focused.
|
|
for (const activeHostTerminal of this._hostActiveTerminals.values()) {
|
|
if (activeHostTerminal?.hasFocus) {
|
|
return activeHostTerminal;
|
|
}
|
|
}
|
|
// Fallback to the last recorded active terminal if neither have focus
|
|
return this._activeInstance;
|
|
}
|
|
|
|
private readonly _onDidChangeActiveGroup = new Emitter<ITerminalGroup | undefined>();
|
|
get onDidChangeActiveGroup(): Event<ITerminalGroup | undefined> { return this._onDidChangeActiveGroup.event; }
|
|
private readonly _onDidCreateInstance = new Emitter<ITerminalInstance>();
|
|
get onDidCreateInstance(): Event<ITerminalInstance> { return this._onDidCreateInstance.event; }
|
|
private readonly _onDidDisposeInstance = new Emitter<ITerminalInstance>();
|
|
get onDidDisposeInstance(): Event<ITerminalInstance> { return this._onDidDisposeInstance.event; }
|
|
private readonly _onDidFocusInstance = new Emitter<ITerminalInstance>();
|
|
get onDidFocusInstance(): Event<ITerminalInstance> { return this._onDidFocusInstance.event; }
|
|
private readonly _onDidReceiveProcessId = new Emitter<ITerminalInstance>();
|
|
get onDidReceiveProcessId(): Event<ITerminalInstance> { return this._onDidReceiveProcessId.event; }
|
|
private readonly _onDidReceiveInstanceLinks = new Emitter<ITerminalInstance>();
|
|
get onDidReceiveInstanceLinks(): Event<ITerminalInstance> { return this._onDidReceiveInstanceLinks.event; }
|
|
private readonly _onDidRequestStartExtensionTerminal = new Emitter<IStartExtensionTerminalRequest>();
|
|
get onDidRequestStartExtensionTerminal(): Event<IStartExtensionTerminalRequest> { return this._onDidRequestStartExtensionTerminal.event; }
|
|
private readonly _onDidChangeInstanceDimensions = new Emitter<ITerminalInstance>();
|
|
get onDidChangeInstanceDimensions(): Event<ITerminalInstance> { return this._onDidChangeInstanceDimensions.event; }
|
|
private readonly _onDidMaxiumumDimensionsChange = new Emitter<ITerminalInstance>();
|
|
get onDidMaximumDimensionsChange(): Event<ITerminalInstance> { return this._onDidMaxiumumDimensionsChange.event; }
|
|
private readonly _onDidChangeInstances = new Emitter<void>();
|
|
get onDidChangeInstances(): Event<void> { return this._onDidChangeInstances.event; }
|
|
private readonly _onDidChangeInstanceTitle = new Emitter<ITerminalInstance | undefined>();
|
|
get onDidChangeInstanceTitle(): Event<ITerminalInstance | undefined> { return this._onDidChangeInstanceTitle.event; }
|
|
private readonly _onDidChangeInstanceIcon = new Emitter<ITerminalInstance | undefined>();
|
|
get onDidChangeInstanceIcon(): Event<ITerminalInstance | undefined> { return this._onDidChangeInstanceIcon.event; }
|
|
private readonly _onDidChangeInstanceColor = new Emitter<ITerminalInstance | undefined>();
|
|
get onDidChangeInstanceColor(): Event<ITerminalInstance | undefined> { return this._onDidChangeInstanceColor.event; }
|
|
private readonly _onDidChangeActiveInstance = new Emitter<ITerminalInstance | undefined>();
|
|
get onDidChangeActiveInstance(): Event<ITerminalInstance | undefined> { return this._onDidChangeActiveInstance.event; }
|
|
private readonly _onDidChangeInstancePrimaryStatus = new Emitter<ITerminalInstance>();
|
|
get onDidChangeInstancePrimaryStatus(): Event<ITerminalInstance> { return this._onDidChangeInstancePrimaryStatus.event; }
|
|
private readonly _onDidInputInstanceData = new Emitter<ITerminalInstance>();
|
|
get onDidInputInstanceData(): Event<ITerminalInstance> { return this._onDidInputInstanceData.event; }
|
|
private readonly _onDidDisposeGroup = new Emitter<ITerminalGroup>();
|
|
get onDidDisposeGroup(): Event<ITerminalGroup> { return this._onDidDisposeGroup.event; }
|
|
private readonly _onDidChangeGroups = new Emitter<void>();
|
|
get onDidChangeGroups(): Event<void> { return this._onDidChangeGroups.event; }
|
|
private readonly _onDidRegisterProcessSupport = new Emitter<void>();
|
|
get onDidRegisterProcessSupport(): Event<void> { return this._onDidRegisterProcessSupport.event; }
|
|
private readonly _onDidChangeConnectionState = new Emitter<void>();
|
|
get onDidChangeConnectionState(): Event<void> { return this._onDidChangeConnectionState.event; }
|
|
|
|
constructor(
|
|
@IContextKeyService private _contextKeyService: IContextKeyService,
|
|
@ILifecycleService lifecycleService: ILifecycleService,
|
|
@ILogService private readonly _logService: ILogService,
|
|
@IDialogService private _dialogService: IDialogService,
|
|
@IInstantiationService private _instantiationService: IInstantiationService,
|
|
@IRemoteAgentService private _remoteAgentService: IRemoteAgentService,
|
|
@IViewsService private _viewsService: IViewsService,
|
|
@IWorkbenchEnvironmentService private readonly _environmentService: IWorkbenchEnvironmentService,
|
|
@ITelemetryService private readonly _telemetryService: ITelemetryService,
|
|
@ITerminalEditorService private readonly _terminalEditorService: ITerminalEditorService,
|
|
@ITerminalGroupService private readonly _terminalGroupService: ITerminalGroupService,
|
|
@ITerminalInstanceService private readonly _terminalInstanceService: ITerminalInstanceService,
|
|
@IEditorGroupsService private readonly _editorGroupsService: IEditorGroupsService,
|
|
@ITerminalProfileService private readonly _terminalProfileService: ITerminalProfileService,
|
|
@IExtensionService private readonly _extensionService: IExtensionService,
|
|
@INotificationService private readonly _notificationService: INotificationService
|
|
) {
|
|
this._configHelper = this._instantiationService.createInstance(TerminalConfigHelper);
|
|
// the below avoids having to poll routinely.
|
|
// we update detected profiles when an instance is created so that,
|
|
// for example, we detect if you've installed a pwsh
|
|
this.onDidCreateInstance(() => this._terminalProfileService.refreshAvailableProfiles());
|
|
|
|
this._forwardInstanceHostEvents(this._terminalGroupService);
|
|
this._forwardInstanceHostEvents(this._terminalEditorService);
|
|
this._terminalGroupService.onDidChangeActiveGroup(this._onDidChangeActiveGroup.fire, this._onDidChangeActiveGroup);
|
|
this._terminalInstanceService.onDidCreateInstance(instance => {
|
|
this._initInstanceListeners(instance);
|
|
this._onDidCreateInstance.fire(instance);
|
|
});
|
|
|
|
this.onDidReceiveInstanceLinks(instance => this._setInstanceLinkProviders(instance));
|
|
|
|
// Hide the panel if there are no more instances, provided that VS Code is not shutting
|
|
// down. When shutting down the panel is locked in place so that it is restored upon next
|
|
// launch.
|
|
this._terminalGroupService.onDidChangeActiveInstance(instance => {
|
|
if (!instance && !this._isShuttingDown) {
|
|
this._terminalGroupService.hidePanel();
|
|
}
|
|
});
|
|
|
|
this._handleInstanceContextKeys();
|
|
this._processSupportContextKey = TerminalContextKeys.processSupported.bindTo(this._contextKeyService);
|
|
this._processSupportContextKey.set(!isWeb || this._remoteAgentService.getConnection() !== null);
|
|
this._terminalHasBeenCreated = TerminalContextKeys.terminalHasBeenCreated.bindTo(this._contextKeyService);
|
|
this._terminalCountContextKey = TerminalContextKeys.count.bindTo(this._contextKeyService);
|
|
|
|
lifecycleService.onBeforeShutdown(async e => e.veto(this._onBeforeShutdown(e.reason), 'veto.terminal'));
|
|
lifecycleService.onWillShutdown(e => this._onWillShutdown(e));
|
|
|
|
// Create async as the class depends on `this`
|
|
timeout(0).then(() => this._instantiationService.createInstance(TerminalEditorStyle, document.head));
|
|
}
|
|
|
|
async showProfileQuickPick(type: 'setDefault' | 'createInstance', cwd?: string | URI): Promise<ITerminalInstance | undefined> {
|
|
const quickPick = this._instantiationService.createInstance(TerminalProfileQuickpick);
|
|
const result = await quickPick.showAndGetResult(type);
|
|
if (!result) {
|
|
return;
|
|
}
|
|
if (typeof result === 'string') {
|
|
return;
|
|
}
|
|
let keyMods: IKeyMods | undefined = result.keyMods;
|
|
if (type === 'createInstance') {
|
|
const activeInstance = this.getDefaultInstanceHost().activeInstance;
|
|
let instance;
|
|
|
|
if (result.config && 'id' in result?.config) {
|
|
await this.createContributedTerminalProfile(result.config.extensionIdentifier, result.config.id, {
|
|
icon: result.config.options?.icon,
|
|
color: result.config.options?.color,
|
|
location: !!(keyMods?.alt && activeInstance) ? { splitActiveTerminal: true } : this.defaultLocation
|
|
});
|
|
return;
|
|
} else if (result.config && 'profileName' in result.config) {
|
|
if (keyMods?.alt && activeInstance) {
|
|
// create split, only valid if there's an active instance
|
|
instance = await this.createTerminal({ location: { parentTerminal: activeInstance }, config: result.config });
|
|
} else {
|
|
instance = await this.createTerminal({ location: this.defaultLocation, config: result.config, cwd });
|
|
}
|
|
}
|
|
|
|
if (instance && this.defaultLocation !== TerminalLocation.Editor) {
|
|
this._terminalGroupService.showPanel(true);
|
|
this.setActiveInstance(instance);
|
|
return instance;
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
handleNewRegisteredBackend(backend: ITerminalBackend) {
|
|
if (backend.remoteAuthority === this._environmentService.remoteAuthority) {
|
|
this._primaryBackend = backend;
|
|
const enableTerminalReconnection = this.configHelper.config.enablePersistentSessions;
|
|
|
|
// Connect to the extension host if it's there, set the connection state to connected when
|
|
// it's done. This should happen even when there is no extension host.
|
|
this._connectionState = TerminalConnectionState.Connecting;
|
|
|
|
const isPersistentRemote = !!this._environmentService.remoteAuthority && enableTerminalReconnection;
|
|
|
|
if (isPersistentRemote) {
|
|
this._remoteTerminalsInitPromise = this._reconnectToRemoteTerminals();
|
|
} else if (enableTerminalReconnection) {
|
|
this._localTerminalsInitPromise = this._reconnectToLocalTerminals();
|
|
} else {
|
|
this._connectionState = TerminalConnectionState.Connected;
|
|
}
|
|
|
|
backend.onDidRequestDetach(async (e) => {
|
|
const instanceToDetach = this.getInstanceFromResource(getTerminalUri(e.workspaceId, e.instanceId));
|
|
if (instanceToDetach) {
|
|
const persistentProcessId = instanceToDetach?.persistentProcessId;
|
|
if (persistentProcessId && !instanceToDetach.shellLaunchConfig.isFeatureTerminal && !instanceToDetach.shellLaunchConfig.customPtyImplementation) {
|
|
if (instanceToDetach.target === TerminalLocation.Editor) {
|
|
this._terminalEditorService.detachInstance(instanceToDetach);
|
|
} else {
|
|
this._terminalGroupService.getGroupForInstance(instanceToDetach)?.removeInstance(instanceToDetach);
|
|
}
|
|
await instanceToDetach.detachFromProcess();
|
|
await this._primaryBackend?.acceptDetachInstanceReply(e.requestId, persistentProcessId);
|
|
} else {
|
|
// will get rejected without a persistentProcessId to attach to
|
|
await this._primaryBackend?.acceptDetachInstanceReply(e.requestId, undefined);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
getPrimaryBackend(): ITerminalBackend | undefined {
|
|
return this._primaryBackend;
|
|
}
|
|
|
|
private _forwardInstanceHostEvents(host: ITerminalInstanceHost) {
|
|
host.onDidChangeInstances(this._onDidChangeInstances.fire, this._onDidChangeInstances);
|
|
host.onDidDisposeInstance(this._onDidDisposeInstance.fire, this._onDidDisposeInstance);
|
|
host.onDidChangeActiveInstance(instance => this._evaluateActiveInstance(host, instance));
|
|
host.onDidFocusInstance(instance => {
|
|
this._onDidFocusInstance.fire(instance);
|
|
this._evaluateActiveInstance(host, instance);
|
|
});
|
|
this._hostActiveTerminals.set(host, undefined);
|
|
}
|
|
|
|
private _evaluateActiveInstance(host: ITerminalInstanceHost, instance: ITerminalInstance | undefined) {
|
|
// Track the latest active terminal for each host so that when one becomes undefined, the
|
|
// TerminalService's active terminal is set to the last active terminal from the other host.
|
|
// This means if the last terminal editor is closed such that it becomes undefined, the last
|
|
// active group's terminal will be used as the active terminal if available.
|
|
this._hostActiveTerminals.set(host, instance);
|
|
if (instance === undefined) {
|
|
for (const active of this._hostActiveTerminals.values()) {
|
|
if (active) {
|
|
instance = active;
|
|
}
|
|
}
|
|
}
|
|
this._activeInstance = instance;
|
|
this._onDidChangeActiveInstance.fire(instance);
|
|
}
|
|
|
|
setActiveInstance(value: ITerminalInstance) {
|
|
// If this was a hideFromUser terminal created by the API this was triggered by show,
|
|
// in which case we need to create the terminal group
|
|
if (value.shellLaunchConfig.hideFromUser) {
|
|
this._showBackgroundTerminal(value);
|
|
}
|
|
if (value.target === TerminalLocation.Editor) {
|
|
this._terminalEditorService.setActiveInstance(value);
|
|
} else {
|
|
this._terminalGroupService.setActiveInstance(value);
|
|
}
|
|
}
|
|
|
|
async createContributedTerminalProfile(extensionIdentifier: string, id: string, options: ICreateContributedTerminalProfileOptions): Promise<void> {
|
|
await this._extensionService.activateByEvent(`onTerminalProfile:${id}`);
|
|
|
|
const profileProvider = this._terminalProfileService.getContributedProfileProvider(extensionIdentifier, id);
|
|
if (!profileProvider) {
|
|
this._notificationService.error(`No terminal profile provider registered for id "${id}"`);
|
|
return;
|
|
}
|
|
try {
|
|
await profileProvider.createContributedTerminalProfile(options);
|
|
this._terminalGroupService.setActiveInstanceByIndex(this._terminalGroupService.instances.length - 1);
|
|
await this._terminalGroupService.activeInstance?.focusWhenReady();
|
|
} catch (e) {
|
|
this._notificationService.error(e.message);
|
|
}
|
|
}
|
|
|
|
async safeDisposeTerminal(instance: ITerminalInstance): Promise<void> {
|
|
// Confirm on kill in the editor is handled by the editor input
|
|
if (instance.target !== TerminalLocation.Editor &&
|
|
instance.hasChildProcesses &&
|
|
(this.configHelper.config.confirmOnKill === 'panel' || this.configHelper.config.confirmOnKill === 'always')) {
|
|
|
|
const veto = await this._showTerminalCloseConfirmation(true);
|
|
if (veto) {
|
|
return;
|
|
}
|
|
}
|
|
instance.dispose();
|
|
}
|
|
|
|
private _setConnected() {
|
|
this._connectionState = TerminalConnectionState.Connected;
|
|
this._onDidChangeConnectionState.fire();
|
|
}
|
|
|
|
private async _reconnectToRemoteTerminals(): Promise<void> {
|
|
const remoteAuthority = this._environmentService.remoteAuthority;
|
|
if (!remoteAuthority) {
|
|
return;
|
|
}
|
|
const backend = this._terminalInstanceService.getBackend(remoteAuthority);
|
|
if (!backend) {
|
|
return;
|
|
}
|
|
const layoutInfo = await backend.getTerminalLayoutInfo();
|
|
backend.reduceConnectionGraceTime();
|
|
const reconnectCounter = await this._recreateTerminalGroups(layoutInfo);
|
|
/* __GDPR__
|
|
"terminalReconnection" : {
|
|
"count" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }
|
|
}
|
|
*/
|
|
const data = {
|
|
count: reconnectCounter
|
|
};
|
|
this._telemetryService.publicLog('terminalReconnection', data);
|
|
// now that terminals have been restored,
|
|
// attach listeners to update remote when terminals are changed
|
|
this._attachProcessLayoutListeners();
|
|
}
|
|
|
|
private async _reconnectToLocalTerminals(): Promise<void> {
|
|
const localBackend = this._terminalInstanceService.getBackend();
|
|
if (!localBackend) {
|
|
return;
|
|
}
|
|
const layoutInfo = await localBackend.getTerminalLayoutInfo();
|
|
if (layoutInfo && layoutInfo.tabs.length > 0) {
|
|
await this._recreateTerminalGroups(layoutInfo);
|
|
}
|
|
// now that terminals have been restored,
|
|
// attach listeners to update local state when terminals are changed
|
|
this._attachProcessLayoutListeners();
|
|
}
|
|
|
|
private async _recreateTerminalGroups(layoutInfo?: ITerminalsLayoutInfo): Promise<number> {
|
|
let reconnectCounter = 0;
|
|
let activeGroup: ITerminalGroup | undefined;
|
|
if (layoutInfo) {
|
|
for (const groupLayout of layoutInfo.tabs) {
|
|
const terminalLayouts = groupLayout.terminals.filter(t => t.terminal && t.terminal.isOrphan);
|
|
if (terminalLayouts.length) {
|
|
reconnectCounter += terminalLayouts.length;
|
|
let terminalInstance: ITerminalInstance | undefined;
|
|
let group: ITerminalGroup | undefined;
|
|
for (const terminalLayout of terminalLayouts) {
|
|
if (!terminalInstance) {
|
|
// create group and terminal
|
|
terminalInstance = await this.createTerminal({
|
|
config: { attachPersistentProcess: terminalLayout.terminal! },
|
|
location: TerminalLocation.Panel
|
|
});
|
|
group = this._terminalGroupService.getGroupForInstance(terminalInstance);
|
|
if (groupLayout.isActive) {
|
|
activeGroup = group;
|
|
}
|
|
} else {
|
|
// add split terminals to this group
|
|
terminalInstance = await this.createTerminal({ config: { attachPersistentProcess: terminalLayout.terminal! }, location: { parentTerminal: terminalInstance } });
|
|
}
|
|
}
|
|
const activeInstance = this.instances.find(t => {
|
|
return t.shellLaunchConfig.attachPersistentProcess?.id === groupLayout.activePersistentProcessId;
|
|
});
|
|
if (activeInstance) {
|
|
this.setActiveInstance(activeInstance);
|
|
}
|
|
group?.resizePanes(groupLayout.terminals.map(terminal => terminal.relativeSize));
|
|
}
|
|
}
|
|
if (layoutInfo.tabs.length) {
|
|
this._terminalGroupService.activeGroup = activeGroup;
|
|
}
|
|
}
|
|
return reconnectCounter;
|
|
}
|
|
|
|
private _attachProcessLayoutListeners(): void {
|
|
this.onDidChangeActiveGroup(() => this._saveState());
|
|
this.onDidChangeActiveInstance(() => this._saveState());
|
|
this.onDidChangeInstances(() => this._saveState());
|
|
// The state must be updated when the terminal is relaunched, otherwise the persistent
|
|
// terminal ID will be stale and the process will be leaked.
|
|
this.onDidReceiveProcessId(() => this._saveState());
|
|
this.onDidChangeInstanceTitle(instance => this._updateTitle(instance));
|
|
this.onDidChangeInstanceIcon(instance => this._updateIcon(instance));
|
|
}
|
|
|
|
private _handleInstanceContextKeys(): void {
|
|
const terminalIsOpenContext = TerminalContextKeys.isOpen.bindTo(this._contextKeyService);
|
|
const updateTerminalContextKeys = () => {
|
|
terminalIsOpenContext.set(this.instances.length > 0);
|
|
this._terminalCountContextKey.set(this.instances.length);
|
|
};
|
|
this.onDidChangeInstances(() => updateTerminalContextKeys());
|
|
}
|
|
|
|
async getActiveOrCreateInstance(): Promise<ITerminalInstance> {
|
|
return this.activeInstance || this.createTerminal();
|
|
}
|
|
|
|
setEditable(instance: ITerminalInstance, data?: IEditableData | null): void {
|
|
if (!data) {
|
|
this._editable = undefined;
|
|
} else {
|
|
this._editable = { instance: instance, data };
|
|
}
|
|
const pane = this._viewsService.getActiveViewWithId<TerminalViewPane>(TERMINAL_VIEW_ID);
|
|
const isEditing = this.isEditable(instance);
|
|
pane?.terminalTabbedView?.setEditable(isEditing);
|
|
}
|
|
|
|
isEditable(instance: ITerminalInstance | undefined): boolean {
|
|
return !!this._editable && (this._editable.instance === instance || !instance);
|
|
}
|
|
|
|
getEditableData(instance: ITerminalInstance): IEditableData | undefined {
|
|
return this._editable && this._editable.instance === instance ? this._editable.data : undefined;
|
|
}
|
|
|
|
requestStartExtensionTerminal(proxy: ITerminalProcessExtHostProxy, cols: number, rows: number): Promise<ITerminalLaunchError | undefined> {
|
|
// The initial request came from the extension host, no need to wait for it
|
|
return new Promise<ITerminalLaunchError | undefined>(callback => {
|
|
this._onDidRequestStartExtensionTerminal.fire({ proxy, cols, rows, callback });
|
|
});
|
|
}
|
|
|
|
private _onBeforeShutdown(reason: ShutdownReason): boolean | Promise<boolean> {
|
|
// Never veto on web as this would block all windows from being closed. This disables
|
|
// process revive as we can't handle it on shutdown.
|
|
if (isWeb) {
|
|
this._isShuttingDown = true;
|
|
return false;
|
|
}
|
|
return this._onBeforeShutdownAsync(reason);
|
|
}
|
|
|
|
private async _onBeforeShutdownAsync(reason: ShutdownReason): Promise<boolean> {
|
|
if (this.instances.length === 0) {
|
|
// No terminal instances, don't veto
|
|
return false;
|
|
}
|
|
|
|
// Persist terminal _buffer state_, note that even if this happens the dirty terminal prompt
|
|
// still shows as that cannot be revived
|
|
try {
|
|
this._shutdownWindowCount = await this._nativeDelegate?.getWindowCount();
|
|
const shouldReviveProcesses = this._shouldReviveProcesses(reason);
|
|
if (shouldReviveProcesses) {
|
|
// Attempt to persist the terminal state but only allow 2000ms as we can't block
|
|
// shutdown. This can happen when in a remote workspace but the other side has been
|
|
// suspended and is in the process of reconnecting, the message will be put in a
|
|
// queue in this case for when the connection is back up and running. Aborting the
|
|
// process is preferable in this case.
|
|
await Promise.race([
|
|
this._primaryBackend?.persistTerminalState(),
|
|
timeout(2000)
|
|
]);
|
|
}
|
|
|
|
// Persist terminal _processes_
|
|
const shouldPersistProcesses = this._configHelper.config.enablePersistentSessions && reason === ShutdownReason.RELOAD;
|
|
if (!shouldPersistProcesses) {
|
|
const hasDirtyInstances = (
|
|
(this.configHelper.config.confirmOnExit === 'always' && this.instances.length > 0) ||
|
|
(this.configHelper.config.confirmOnExit === 'hasChildProcesses' && this.instances.some(e => e.hasChildProcesses))
|
|
);
|
|
if (hasDirtyInstances) {
|
|
return this._onBeforeShutdownConfirmation(reason);
|
|
}
|
|
}
|
|
} catch (err: unknown) {
|
|
// Swallow as exceptions should not cause a veto to prevent shutdown
|
|
this._logService.warn('Exception occurred during terminal shutdown', err);
|
|
}
|
|
|
|
this._isShuttingDown = true;
|
|
|
|
return false;
|
|
}
|
|
|
|
setNativeDelegate(nativeDelegate: ITerminalServiceNativeDelegate): void {
|
|
this._nativeDelegate = nativeDelegate;
|
|
}
|
|
|
|
async toggleDevTools(open?: boolean): Promise<void> {
|
|
if (open) {
|
|
this._nativeDelegate?.openDevTools();
|
|
} else {
|
|
this._nativeDelegate?.toggleDevTools();
|
|
}
|
|
}
|
|
private _shouldReviveProcesses(reason: ShutdownReason): boolean {
|
|
if (!this._configHelper.config.enablePersistentSessions) {
|
|
return false;
|
|
}
|
|
switch (this.configHelper.config.persistentSessionReviveProcess) {
|
|
case 'onExit': {
|
|
// Allow on close if it's the last window on Windows or Linux
|
|
if (reason === ShutdownReason.CLOSE && (this._shutdownWindowCount === 1 && !isMacintosh)) {
|
|
return true;
|
|
}
|
|
return reason === ShutdownReason.LOAD || reason === ShutdownReason.QUIT;
|
|
}
|
|
case 'onExitAndWindowClose': return reason !== ShutdownReason.RELOAD;
|
|
default: return false;
|
|
}
|
|
}
|
|
|
|
private async _onBeforeShutdownConfirmation(reason: ShutdownReason): Promise<boolean> {
|
|
// veto if configured to show confirmation and the user chose not to exit
|
|
const veto = await this._showTerminalCloseConfirmation();
|
|
if (!veto) {
|
|
this._isShuttingDown = true;
|
|
}
|
|
|
|
return veto;
|
|
}
|
|
|
|
private _onWillShutdown(e: WillShutdownEvent): void {
|
|
// Don't touch processes if the shutdown was a result of reload as they will be reattached
|
|
const shouldPersistTerminals = this._configHelper.config.enablePersistentSessions && e.reason === ShutdownReason.RELOAD;
|
|
if (shouldPersistTerminals) {
|
|
for (const instance of this.instances) {
|
|
instance.detachFromProcess();
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Force dispose of all terminal instances
|
|
for (const instance of this.instances) {
|
|
instance.dispose();
|
|
}
|
|
|
|
// Clear terminal layout info only when not persisting
|
|
if (!this._shouldReviveProcesses(e.reason)) {
|
|
this._primaryBackend?.setTerminalLayoutInfo(undefined);
|
|
}
|
|
}
|
|
|
|
getFindState(): FindReplaceState {
|
|
return this._findState;
|
|
}
|
|
|
|
@debounce(500)
|
|
private _saveState(): void {
|
|
// Avoid saving state when shutting down as that would override process state to be revived
|
|
if (this._isShuttingDown) {
|
|
return;
|
|
}
|
|
if (!this.configHelper.config.enablePersistentSessions) {
|
|
return;
|
|
}
|
|
const tabs = this._terminalGroupService.groups.map(g => g.getLayoutInfo(g === this._terminalGroupService.activeGroup));
|
|
const state: ITerminalsLayoutInfoById = { tabs };
|
|
this._primaryBackend?.setTerminalLayoutInfo(state);
|
|
}
|
|
|
|
@debounce(500)
|
|
private _updateTitle(instance?: ITerminalInstance): void {
|
|
if (!this.configHelper.config.enablePersistentSessions || !instance || !instance.persistentProcessId || !instance.title) {
|
|
return;
|
|
}
|
|
this._primaryBackend?.updateTitle(instance.persistentProcessId, instance.title, instance.titleSource);
|
|
}
|
|
|
|
@debounce(500)
|
|
private _updateIcon(instance?: ITerminalInstance): void {
|
|
if (!this.configHelper.config.enablePersistentSessions || !instance || !instance.persistentProcessId || !instance.icon) {
|
|
return;
|
|
}
|
|
this._primaryBackend?.updateIcon(instance.persistentProcessId, instance.icon, instance.color);
|
|
}
|
|
|
|
refreshActiveGroup(): void {
|
|
this._onDidChangeActiveGroup.fire(this._terminalGroupService.activeGroup);
|
|
}
|
|
|
|
doWithActiveInstance<T>(callback: (terminal: ITerminalInstance) => T): T | void {
|
|
const instance = this.activeInstance;
|
|
if (instance) {
|
|
return callback(instance);
|
|
}
|
|
}
|
|
|
|
getInstanceFromId(terminalId: number): ITerminalInstance | undefined {
|
|
let bgIndex = -1;
|
|
this._backgroundedTerminalInstances.forEach((terminalInstance, i) => {
|
|
if (terminalInstance.instanceId === terminalId) {
|
|
bgIndex = i;
|
|
}
|
|
});
|
|
if (bgIndex !== -1) {
|
|
return this._backgroundedTerminalInstances[bgIndex];
|
|
}
|
|
try {
|
|
return this.instances[this._getIndexFromId(terminalId)];
|
|
} catch {
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
getInstanceFromIndex(terminalIndex: number): ITerminalInstance {
|
|
return this.instances[terminalIndex];
|
|
}
|
|
|
|
getInstanceFromResource(resource: URI | undefined): ITerminalInstance | undefined {
|
|
return getInstanceFromResource(this.instances, resource);
|
|
}
|
|
|
|
isAttachedToTerminal(remoteTerm: IRemoteTerminalAttachTarget): boolean {
|
|
return this.instances.some(term => term.processId === remoteTerm.pid);
|
|
}
|
|
|
|
async initializeTerminals(): Promise<void> {
|
|
if (this._remoteTerminalsInitPromise) {
|
|
await this._remoteTerminalsInitPromise;
|
|
this._setConnected();
|
|
} else if (this._localTerminalsInitPromise) {
|
|
await this._localTerminalsInitPromise;
|
|
this._setConnected();
|
|
}
|
|
if (this._terminalGroupService.groups.length === 0 && this.isProcessSupportRegistered) {
|
|
this.createTerminal({ location: TerminalLocation.Panel });
|
|
}
|
|
}
|
|
|
|
moveToEditor(source: ITerminalInstance): void {
|
|
if (source.target === TerminalLocation.Editor) {
|
|
return;
|
|
}
|
|
const sourceGroup = this._terminalGroupService.getGroupForInstance(source);
|
|
if (!sourceGroup) {
|
|
return;
|
|
}
|
|
sourceGroup.removeInstance(source);
|
|
this._terminalEditorService.openEditor(source);
|
|
}
|
|
|
|
async moveToTerminalView(source?: ITerminalInstance, target?: ITerminalInstance, side?: 'before' | 'after'): Promise<void> {
|
|
if (URI.isUri(source)) {
|
|
source = this.getInstanceFromResource(source);
|
|
}
|
|
|
|
if (source) {
|
|
this._terminalEditorService.detachInstance(source);
|
|
} else {
|
|
source = this._terminalEditorService.detachActiveEditorInstance();
|
|
if (!source) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (source.target !== TerminalLocation.Editor) {
|
|
await this._terminalGroupService.showPanel(true);
|
|
return;
|
|
}
|
|
source.target = TerminalLocation.Panel;
|
|
|
|
let group: ITerminalGroup | undefined;
|
|
if (target) {
|
|
group = this._terminalGroupService.getGroupForInstance(target);
|
|
}
|
|
|
|
if (!group) {
|
|
group = this._terminalGroupService.createGroup();
|
|
}
|
|
|
|
group.addInstance(source);
|
|
this.setActiveInstance(source);
|
|
await this._terminalGroupService.showPanel(true);
|
|
// TODO: Shouldn't this happen automatically?
|
|
source.setVisible(true);
|
|
|
|
if (target && side) {
|
|
const index = group.terminalInstances.indexOf(target) + (side === 'after' ? 1 : 0);
|
|
group.moveInstance(source, index);
|
|
}
|
|
|
|
// Fire events
|
|
this._onDidChangeInstances.fire();
|
|
this._onDidChangeActiveGroup.fire(this._terminalGroupService.activeGroup);
|
|
this._terminalGroupService.showPanel(true);
|
|
}
|
|
|
|
protected _initInstanceListeners(instance: ITerminalInstance): void {
|
|
instance.addDisposable(instance.onTitleChanged(this._onDidChangeInstanceTitle.fire, this._onDidChangeInstanceTitle));
|
|
instance.addDisposable(instance.onIconChanged(this._onDidChangeInstanceIcon.fire, this._onDidChangeInstanceIcon));
|
|
instance.addDisposable(instance.onIconChanged(this._onDidChangeInstanceColor.fire, this._onDidChangeInstanceColor));
|
|
instance.addDisposable(instance.onProcessIdReady(this._onDidReceiveProcessId.fire, this._onDidReceiveProcessId));
|
|
instance.addDisposable(instance.statusList.onDidChangePrimaryStatus(() => this._onDidChangeInstancePrimaryStatus.fire(instance)));
|
|
instance.addDisposable(instance.onLinksReady(this._onDidReceiveInstanceLinks.fire, this._onDidReceiveInstanceLinks));
|
|
instance.addDisposable(instance.onDimensionsChanged(() => {
|
|
this._onDidChangeInstanceDimensions.fire(instance);
|
|
if (this.configHelper.config.enablePersistentSessions && this.isProcessSupportRegistered) {
|
|
this._saveState();
|
|
}
|
|
}));
|
|
instance.addDisposable(instance.onMaximumDimensionsChanged(() => this._onDidMaxiumumDimensionsChange.fire(instance)));
|
|
instance.addDisposable(instance.onDidInputData(this._onDidInputInstanceData.fire, this._onDidInputInstanceData));
|
|
instance.addDisposable(instance.onDidFocus(this._onDidChangeActiveInstance.fire, this._onDidChangeActiveInstance));
|
|
instance.addDisposable(instance.onRequestAddInstanceToGroup(async e => await this._addInstanceToGroup(instance, e)));
|
|
}
|
|
|
|
private async _addInstanceToGroup(instance: ITerminalInstance, e: IRequestAddInstanceToGroupEvent): Promise<void> {
|
|
const terminalIdentifier = parseTerminalUri(e.uri);
|
|
if (terminalIdentifier.instanceId === undefined) {
|
|
return;
|
|
}
|
|
|
|
let sourceInstance: ITerminalInstance | undefined = this.getInstanceFromResource(e.uri);
|
|
|
|
// Terminal from a different window
|
|
if (!sourceInstance) {
|
|
const attachPersistentProcess = await this._primaryBackend?.requestDetachInstance(terminalIdentifier.workspaceId, terminalIdentifier.instanceId);
|
|
if (attachPersistentProcess) {
|
|
sourceInstance = await this.createTerminal({ config: { attachPersistentProcess }, resource: e.uri });
|
|
this._terminalGroupService.moveInstance(sourceInstance, instance, e.side);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// View terminals
|
|
sourceInstance = this._terminalGroupService.getInstanceFromResource(e.uri);
|
|
if (sourceInstance) {
|
|
this._terminalGroupService.moveInstance(sourceInstance, instance, e.side);
|
|
return;
|
|
}
|
|
|
|
// Terminal editors
|
|
sourceInstance = this._terminalEditorService.getInstanceFromResource(e.uri);
|
|
if (sourceInstance) {
|
|
this.moveToTerminalView(sourceInstance, instance, e.side);
|
|
return;
|
|
}
|
|
return;
|
|
}
|
|
|
|
registerProcessSupport(isSupported: boolean): void {
|
|
if (!isSupported) {
|
|
return;
|
|
}
|
|
this._processSupportContextKey.set(isSupported);
|
|
this._onDidRegisterProcessSupport.fire();
|
|
}
|
|
|
|
registerLinkProvider(linkProvider: ITerminalExternalLinkProvider): IDisposable {
|
|
const disposables: IDisposable[] = [];
|
|
this._linkProviders.add(linkProvider);
|
|
for (const instance of this.instances) {
|
|
if (instance.areLinksReady) {
|
|
disposables.push(instance.registerLinkProvider(linkProvider));
|
|
}
|
|
}
|
|
this._linkProviderDisposables.set(linkProvider, disposables);
|
|
return {
|
|
dispose: () => {
|
|
const disposables = this._linkProviderDisposables.get(linkProvider) || [];
|
|
for (const disposable of disposables) {
|
|
disposable.dispose();
|
|
}
|
|
this._linkProviders.delete(linkProvider);
|
|
}
|
|
};
|
|
}
|
|
|
|
private _setInstanceLinkProviders(instance: ITerminalInstance): void {
|
|
for (const linkProvider of this._linkProviders) {
|
|
const disposables = this._linkProviderDisposables.get(linkProvider);
|
|
const provider = instance.registerLinkProvider(linkProvider);
|
|
disposables?.push(provider);
|
|
}
|
|
}
|
|
|
|
// TODO: Remove this, it should live in group/editor servioce
|
|
private _getIndexFromId(terminalId: number): number {
|
|
let terminalIndex = -1;
|
|
this.instances.forEach((terminalInstance, i) => {
|
|
if (terminalInstance.instanceId === terminalId) {
|
|
terminalIndex = i;
|
|
}
|
|
});
|
|
if (terminalIndex === -1) {
|
|
throw new Error(`Terminal with ID ${terminalId} does not exist (has it already been disposed?)`);
|
|
}
|
|
return terminalIndex;
|
|
}
|
|
|
|
protected async _showTerminalCloseConfirmation(singleTerminal?: boolean): Promise<boolean> {
|
|
let message: string;
|
|
if (this.instances.length === 1 || singleTerminal) {
|
|
message = nls.localize('terminalService.terminalCloseConfirmationSingular', "Do you want to terminate the active terminal session?");
|
|
} else {
|
|
message = nls.localize('terminalService.terminalCloseConfirmationPlural', "Do you want to terminal the {0} active terminal sessions?", this.instances.length);
|
|
}
|
|
const res = await this._dialogService.confirm({
|
|
message,
|
|
primaryButton: nls.localize('terminate', "Terminate"),
|
|
type: 'warning',
|
|
});
|
|
return !res.confirmed;
|
|
}
|
|
|
|
getDefaultInstanceHost(): ITerminalInstanceHost {
|
|
if (this.defaultLocation === TerminalLocation.Editor) {
|
|
return this._terminalEditorService;
|
|
}
|
|
return this._terminalGroupService;
|
|
}
|
|
|
|
getInstanceHost(location: ITerminalLocationOptions | undefined): ITerminalInstanceHost {
|
|
if (location) {
|
|
if (location === TerminalLocation.Editor) {
|
|
return this._terminalEditorService;
|
|
} else if (typeof location === 'object') {
|
|
if ('viewColumn' in location) {
|
|
return this._terminalEditorService;
|
|
} else if ('parentTerminal' in location) {
|
|
return location.parentTerminal.target === TerminalLocation.Editor ? this._terminalEditorService : this._terminalGroupService;
|
|
}
|
|
} else {
|
|
return this._terminalGroupService;
|
|
}
|
|
}
|
|
return this;
|
|
}
|
|
|
|
getFindHost(instance: ITerminalInstance | undefined = this.activeInstance): ITerminalFindHost {
|
|
return instance?.target === TerminalLocation.Editor ? this._terminalEditorService : this._terminalGroupService;
|
|
}
|
|
|
|
async createTerminal(options?: ICreateTerminalOptions): Promise<ITerminalInstance> {
|
|
// Await the initialization of available profiles as long as this is not a pty terminal or a
|
|
// local terminal in a remote workspace as profile won't be used in those cases and these
|
|
// terminals need to be launched before remote connections are established.
|
|
if (!this._terminalProfileService.availableProfiles) {
|
|
const isPtyTerminal = options?.config && 'customPtyImplementation' in options.config;
|
|
const isLocalInRemoteTerminal = this._remoteAgentService.getConnection() && URI.isUri(options?.cwd) && options?.cwd.scheme === Schemas.vscodeFileResource;
|
|
if (!isPtyTerminal && !isLocalInRemoteTerminal) {
|
|
await this._terminalProfileService.refreshAvailableProfiles();
|
|
}
|
|
}
|
|
|
|
const config = options?.config || this._terminalProfileService.availableProfiles?.find(p => p.profileName === this._terminalProfileService.getDefaultProfileName());
|
|
const shellLaunchConfig = config && 'extensionIdentifier' in config ? {} : this._terminalInstanceService.convertProfileToShellLaunchConfig(config || {});
|
|
|
|
// Get the contributed profile if it was provided
|
|
let contributedProfile = config && 'extensionIdentifier' in config ? config : undefined;
|
|
|
|
// Get the default profile as a contributed profile if it exists
|
|
if (!contributedProfile && (!options || !options.config)) {
|
|
contributedProfile = await this._terminalProfileService.getContributedDefaultProfile(shellLaunchConfig);
|
|
}
|
|
|
|
// Launch the contributed profile
|
|
if (contributedProfile) {
|
|
const resolvedLocation = this.resolveLocation(options?.location);
|
|
const splitActiveTerminal = typeof options?.location === 'object' && 'splitActiveTerminal' in options.location ? options.location.splitActiveTerminal : typeof options?.location === 'object' ? 'parentTerminal' in options.location : false;
|
|
let location: TerminalLocation | { viewColumn: number, preserveState?: boolean } | { splitActiveTerminal: boolean } | undefined;
|
|
if (splitActiveTerminal) {
|
|
location = resolvedLocation === TerminalLocation.Editor ? { viewColumn: SIDE_GROUP } : { splitActiveTerminal: true };
|
|
} else {
|
|
location = typeof options?.location === 'object' && 'viewColumn' in options.location ? options.location : resolvedLocation;
|
|
}
|
|
await this.createContributedTerminalProfile(contributedProfile.extensionIdentifier, contributedProfile.id, {
|
|
icon: contributedProfile.icon,
|
|
color: contributedProfile.color,
|
|
location
|
|
});
|
|
const instanceHost = resolvedLocation === TerminalLocation.Editor ? this._terminalEditorService : this._terminalGroupService;
|
|
const instance = instanceHost.instances[instanceHost.instances.length - 1];
|
|
await instance.focusWhenReady();
|
|
this._terminalHasBeenCreated.set(true);
|
|
return instance;
|
|
}
|
|
|
|
if (options?.cwd) {
|
|
shellLaunchConfig.cwd = options.cwd;
|
|
}
|
|
|
|
if (!shellLaunchConfig.customPtyImplementation && !this.isProcessSupportRegistered) {
|
|
throw new Error('Could not create terminal when process support is not registered');
|
|
}
|
|
if (shellLaunchConfig.hideFromUser) {
|
|
const instance = this._terminalInstanceService.createInstance(shellLaunchConfig, undefined, options?.resource);
|
|
this._backgroundedTerminalInstances.push(instance);
|
|
this._backgroundedTerminalDisposables.set(instance.instanceId, [
|
|
instance.onDisposed(this._onDidDisposeInstance.fire, this._onDidDisposeInstance)
|
|
]);
|
|
this._terminalHasBeenCreated.set(true);
|
|
return instance;
|
|
}
|
|
|
|
this._evaluateLocalCwd(shellLaunchConfig);
|
|
const location = this.resolveLocation(options?.location) || this.defaultLocation;
|
|
const parent = this._getSplitParent(options?.location);
|
|
this._terminalHasBeenCreated.set(true);
|
|
if (parent) {
|
|
return this._splitTerminal(shellLaunchConfig, location, parent);
|
|
}
|
|
return this._createTerminal(shellLaunchConfig, location, options);
|
|
}
|
|
|
|
private _splitTerminal(shellLaunchConfig: IShellLaunchConfig, location: TerminalLocation, parent: ITerminalInstance): ITerminalInstance {
|
|
let instance;
|
|
// Use the URI from the base instance if it exists, this will correctly split local terminals
|
|
if (typeof shellLaunchConfig.cwd !== 'object' && typeof parent.shellLaunchConfig.cwd === 'object') {
|
|
shellLaunchConfig.cwd = URI.from({
|
|
scheme: parent.shellLaunchConfig.cwd.scheme,
|
|
authority: parent.shellLaunchConfig.cwd.authority,
|
|
path: shellLaunchConfig.cwd || parent.shellLaunchConfig.cwd.path
|
|
});
|
|
}
|
|
if (location === TerminalLocation.Editor || parent.target === TerminalLocation.Editor) {
|
|
instance = this._terminalEditorService.splitInstance(parent, shellLaunchConfig);
|
|
} else {
|
|
const group = this._terminalGroupService.getGroupForInstance(parent);
|
|
if (!group) {
|
|
throw new Error(`Cannot split a terminal without a group ${parent}`);
|
|
}
|
|
shellLaunchConfig.parentTerminalId = parent.instanceId;
|
|
instance = group.split(shellLaunchConfig);
|
|
this._terminalGroupService.groups.forEach((g, i) => g.setVisible(i === this._terminalGroupService.activeGroupIndex));
|
|
}
|
|
return instance;
|
|
}
|
|
|
|
private _createTerminal(shellLaunchConfig: IShellLaunchConfig, location: TerminalLocation, options?: ICreateTerminalOptions): ITerminalInstance {
|
|
let instance;
|
|
const editorOptions = this._getEditorOptions(options?.location);
|
|
if (location === TerminalLocation.Editor) {
|
|
instance = this._terminalInstanceService.createInstance(shellLaunchConfig, undefined, options?.resource);
|
|
instance.target = TerminalLocation.Editor;
|
|
this._terminalEditorService.openEditor(instance, editorOptions);
|
|
} else {
|
|
// TODO: pass resource?
|
|
const group = this._terminalGroupService.createGroup(shellLaunchConfig);
|
|
instance = group.terminalInstances[0];
|
|
}
|
|
return instance;
|
|
}
|
|
|
|
resolveLocation(location?: ITerminalLocationOptions): TerminalLocation | undefined {
|
|
if (location && typeof location === 'object') {
|
|
if ('parentTerminal' in location) {
|
|
// since we don't set the target unless it's an editor terminal, this is necessary
|
|
return !location.parentTerminal.target ? TerminalLocation.Panel : location.parentTerminal.target;
|
|
} else if ('viewColumn' in location) {
|
|
return TerminalLocation.Editor;
|
|
} else if ('splitActiveTerminal' in location) {
|
|
// since we don't set the target unless it's an editor terminal, this is necessary
|
|
return !this._activeInstance?.target ? TerminalLocation.Panel : this._activeInstance?.target;
|
|
}
|
|
}
|
|
return location;
|
|
}
|
|
|
|
private _getSplitParent(location?: ITerminalLocationOptions): ITerminalInstance | undefined {
|
|
if (location && typeof location === 'object' && 'parentTerminal' in location) {
|
|
return location.parentTerminal;
|
|
} else if (location && typeof location === 'object' && 'splitActiveTerminal' in location) {
|
|
return this.activeInstance;
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
private _getEditorOptions(location?: ITerminalLocationOptions): TerminalEditorLocation | undefined {
|
|
if (location && typeof location === 'object' && 'viewColumn' in location) {
|
|
// When ACTIVE_GROUP is used, resolve it to an actual group to ensure the is created in
|
|
// the active group even if it is locked
|
|
if (location.viewColumn === ACTIVE_GROUP) {
|
|
location.viewColumn = this._editorGroupsService.activeGroup.index;
|
|
}
|
|
return location;
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
private _evaluateLocalCwd(shellLaunchConfig: IShellLaunchConfig) {
|
|
// Add welcome message and title annotation for local terminals launched within remote or
|
|
// virtual workspaces
|
|
if (typeof shellLaunchConfig.cwd !== 'string' && shellLaunchConfig.cwd?.scheme === Schemas.file) {
|
|
if (VirtualWorkspaceContext.getValue(this._contextKeyService)) {
|
|
shellLaunchConfig.initialText = formatMessageForTerminal(nls.localize('localTerminalVirtualWorkspace', "⚠ : This shell is open to a {0}local{1} folder, NOT to the virtual folder", '\x1b[3m', '\x1b[23m'), true);
|
|
shellLaunchConfig.description = nls.localize('localTerminalDescription', "Local");
|
|
} else if (this._remoteAgentService.getConnection()) {
|
|
shellLaunchConfig.initialText = formatMessageForTerminal(nls.localize('localTerminalRemote', "⚠ : This shell is running on your {0}local{1} machine, NOT on the connected remote machine", '\x1b[3m', '\x1b[23m'), true);
|
|
shellLaunchConfig.description = nls.localize('localTerminalDescription', "Local");
|
|
}
|
|
}
|
|
}
|
|
|
|
protected _showBackgroundTerminal(instance: ITerminalInstance): void {
|
|
this._backgroundedTerminalInstances.splice(this._backgroundedTerminalInstances.indexOf(instance), 1);
|
|
const disposables = this._backgroundedTerminalDisposables.get(instance.instanceId);
|
|
if (disposables) {
|
|
dispose(disposables);
|
|
}
|
|
this._backgroundedTerminalDisposables.delete(instance.instanceId);
|
|
instance.shellLaunchConfig.hideFromUser = false;
|
|
this._terminalGroupService.createGroup(instance);
|
|
|
|
// Make active automatically if it's the first instance
|
|
if (this.instances.length === 1) {
|
|
this._terminalGroupService.setActiveInstanceByIndex(0);
|
|
}
|
|
|
|
this._onDidChangeInstances.fire();
|
|
this._onDidChangeGroups.fire();
|
|
}
|
|
|
|
async setContainers(panelContainer: HTMLElement, terminalContainer: HTMLElement): Promise<void> {
|
|
this._configHelper.panelContainer = panelContainer;
|
|
this._terminalGroupService.setContainer(terminalContainer);
|
|
}
|
|
}
|
|
|
|
class TerminalEditorStyle extends Themable {
|
|
private _styleElement: HTMLElement;
|
|
|
|
constructor(
|
|
container: HTMLElement,
|
|
@ITerminalService private readonly _terminalService: ITerminalService,
|
|
@IThemeService private readonly _themeService: IThemeService,
|
|
@ITerminalProfileService private readonly _terminalProfileService: ITerminalProfileService
|
|
) {
|
|
super(_themeService);
|
|
this._registerListeners();
|
|
this._styleElement = document.createElement('style');
|
|
container.appendChild(this._styleElement);
|
|
this._register(toDisposable(() => container.removeChild(this._styleElement)));
|
|
this.updateStyles();
|
|
}
|
|
|
|
private _registerListeners(): void {
|
|
this._register(this._terminalService.onDidChangeInstanceIcon(() => this.updateStyles()));
|
|
this._register(this._terminalService.onDidChangeInstanceColor(() => this.updateStyles()));
|
|
this._register(this._terminalService.onDidCreateInstance(() => this.updateStyles()));
|
|
this._register(this._terminalProfileService.onDidChangeAvailableProfiles(() => this.updateStyles()));
|
|
}
|
|
|
|
override updateStyles(): void {
|
|
super.updateStyles();
|
|
const colorTheme = this._themeService.getColorTheme();
|
|
|
|
// TODO: add a rule collector to avoid duplication
|
|
let css = '';
|
|
|
|
// Add icons
|
|
for (const instance of this._terminalService.instances) {
|
|
const icon = instance.icon;
|
|
if (!icon) {
|
|
continue;
|
|
}
|
|
let uri = undefined;
|
|
if (icon instanceof URI) {
|
|
uri = icon;
|
|
} else if (icon instanceof Object && 'light' in icon && 'dark' in icon) {
|
|
uri = colorTheme.type === ColorScheme.LIGHT ? icon.light : icon.dark;
|
|
}
|
|
const iconClasses = getUriClasses(instance, colorTheme.type);
|
|
if (uri instanceof URI && iconClasses && iconClasses.length > 1) {
|
|
css += (
|
|
`.monaco-workbench .terminal-tab.${iconClasses[0]}::before` +
|
|
`{background-image: ${dom.asCSSUrl(uri)};}`
|
|
);
|
|
}
|
|
if (ThemeIcon.isThemeIcon(icon)) {
|
|
const codicon = iconRegistry.get(icon.id);
|
|
if (codicon) {
|
|
let def: Codicon | IconDefinition = codicon;
|
|
while ('definition' in def) {
|
|
def = def.definition;
|
|
}
|
|
css += (
|
|
`.monaco-workbench .terminal-tab.codicon-${icon.id}::before` +
|
|
`{content: '${def.fontCharacter}' !important;}`
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add colors
|
|
const iconForegroundColor = colorTheme.getColor(iconForeground);
|
|
if (iconForegroundColor) {
|
|
css += `.monaco-workbench .show-file-icons .file-icon.terminal-tab::before { color: ${iconForegroundColor}; }`;
|
|
}
|
|
|
|
css += getColorStyleContent(colorTheme, true);
|
|
this._styleElement.textContent = css;
|
|
}
|
|
}
|