Files
vscode/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts
2025-01-09 06:04:51 -08:00

2727 lines
111 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 { isFirefox } from '../../../../base/browser/browser.js';
import { BrowserFeatures } from '../../../../base/browser/canIUse.js';
import { DataTransfers } from '../../../../base/browser/dnd.js';
import * as dom from '../../../../base/browser/dom.js';
import { StandardKeyboardEvent } from '../../../../base/browser/keyboardEvent.js';
import { Orientation } from '../../../../base/browser/ui/sash/sash.js';
import { DomScrollableElement } from '../../../../base/browser/ui/scrollbar/scrollableElement.js';
import { AutoOpenBarrier, Barrier, Promises, disposableTimeout, timeout } from '../../../../base/common/async.js';
import { Codicon } from '../../../../base/common/codicons.js';
import { debounce } from '../../../../base/common/decorators.js';
import { onUnexpectedError } from '../../../../base/common/errors.js';
import { Emitter, Event } from '../../../../base/common/event.js';
import { KeyCode } from '../../../../base/common/keyCodes.js';
import { ISeparator, template } from '../../../../base/common/labels.js';
import { Disposable, DisposableMap, DisposableStore, IDisposable, ImmortalReference, MutableDisposable, dispose, toDisposable, type IReference } from '../../../../base/common/lifecycle.js';
import { Schemas } from '../../../../base/common/network.js';
import * as path from '../../../../base/common/path.js';
import { OS, OperatingSystem, isMacintosh, isWindows } from '../../../../base/common/platform.js';
import { ScrollbarVisibility } from '../../../../base/common/scrollable.js';
import { URI } from '../../../../base/common/uri.js';
import { TabFocus } from '../../../../editor/browser/config/tabFocus.js';
import * as nls from '../../../../nls.js';
import { IAccessibilityService } from '../../../../platform/accessibility/common/accessibility.js';
import { AccessibilitySignal, IAccessibilitySignalService } from '../../../../platform/accessibilitySignal/browser/accessibilitySignalService.js';
import { ICommandService } from '../../../../platform/commands/common/commands.js';
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
import { CodeDataTransfers, containsDragType, getPathForFile } from '../../../../platform/dnd/browser/dnd.js';
import { FileSystemProviderCapabilities, IFileService } from '../../../../platform/files/common/files.js';
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
import { ServiceCollection } from '../../../../platform/instantiation/common/serviceCollection.js';
import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';
import { ResultKind } from '../../../../platform/keybinding/common/keybindingResolver.js';
import { INotificationService, IPromptChoice, Severity } from '../../../../platform/notification/common/notification.js';
import { IOpenerService } from '../../../../platform/opener/common/opener.js';
import { IProductService } from '../../../../platform/product/common/productService.js';
import { IQuickInputService, IQuickPickItem, QuickPickItem } from '../../../../platform/quickinput/common/quickInput.js';
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';
import { IMarkProperties, TerminalCapability } from '../../../../platform/terminal/common/capabilities/capabilities.js';
import { TerminalCapabilityStoreMultiplexer } from '../../../../platform/terminal/common/capabilities/terminalCapabilityStore.js';
import { IEnvironmentVariableCollection, IMergedEnvironmentVariableCollection } from '../../../../platform/terminal/common/environmentVariable.js';
import { deserializeEnvironmentVariableCollections } from '../../../../platform/terminal/common/environmentVariableShared.js';
import { GeneralShellType, IProcessDataEvent, IProcessPropertyMap, IReconnectionProperties, IShellLaunchConfig, ITerminalDimensionsOverride, ITerminalLaunchError, ITerminalLogService, PosixShellType, ProcessPropertyType, ShellIntegrationStatus, TerminalExitReason, TerminalIcon, TerminalLocation, TerminalSettingId, TerminalShellType, TitleEventSource, WindowsShellType } from '../../../../platform/terminal/common/terminal.js';
import { formatMessageForTerminal } from '../../../../platform/terminal/common/terminalStrings.js';
import { editorBackground } from '../../../../platform/theme/common/colorRegistry.js';
import { getIconRegistry } from '../../../../platform/theme/common/iconRegistry.js';
import { IColorTheme, IThemeService } from '../../../../platform/theme/common/themeService.js';
import { IWorkspaceContextService, IWorkspaceFolder } from '../../../../platform/workspace/common/workspace.js';
import { IWorkspaceTrustRequestService } from '../../../../platform/workspace/common/workspaceTrust.js';
import { PANEL_BACKGROUND, SIDE_BAR_BACKGROUND } from '../../../common/theme.js';
import { IViewDescriptorService, ViewContainerLocation } from '../../../common/views.js';
import { IViewsService } from '../../../services/views/common/viewsService.js';
import { AccessibilityVerbositySettingId } from '../../accessibility/browser/accessibilityConfiguration.js';
import { IRequestAddInstanceToGroupEvent, ITerminalConfigurationService, ITerminalContribution, ITerminalInstance, IXtermColorProvider, TerminalDataTransfers } from './terminal.js';
import { TerminalLaunchHelpAction } from './terminalActions.js';
import { TerminalEditorInput } from './terminalEditorInput.js';
import { TerminalExtensionsRegistry } from './terminalExtensions.js';
import { getColorClass, createColorStyleElement, getStandardColors } from './terminalIcon.js';
import { TerminalProcessManager } from './terminalProcessManager.js';
import { ITerminalStatusList, TerminalStatus, TerminalStatusList } from './terminalStatusList.js';
import { getTerminalResourcesFromDragEvent, getTerminalUri } from './terminalUri.js';
import { TerminalWidgetManager } from './widgets/widgetManager.js';
import { LineDataEventAddon } from './xterm/lineDataEventAddon.js';
import { XtermTerminal, getXtermScaledDimensions } from './xterm/xtermTerminal.js';
import { IEnvironmentVariableInfo } from '../common/environmentVariable.js';
import { DEFAULT_COMMANDS_TO_SKIP_SHELL, ITerminalProcessManager, ITerminalProfileResolverService, ProcessState, TERMINAL_CREATION_COMMANDS, TERMINAL_VIEW_ID, TerminalCommandId } from '../common/terminal.js';
import { TERMINAL_BACKGROUND_COLOR } from '../common/terminalColorRegistry.js';
import { TerminalContextKeys } from '../common/terminalContextKey.js';
import { getWorkspaceForTerminal, preparePathForShell } from '../common/terminalEnvironment.js';
import { IEditorService } from '../../../services/editor/common/editorService.js';
import { IWorkbenchEnvironmentService } from '../../../services/environment/common/environmentService.js';
import { IHistoryService } from '../../../services/history/common/history.js';
import { isHorizontal, IWorkbenchLayoutService } from '../../../services/layout/browser/layoutService.js';
import { IPathService } from '../../../services/path/common/pathService.js';
import { IPreferencesService } from '../../../services/preferences/common/preferences.js';
import { importAMDNodeModule } from '../../../../amdX.js';
import type { IMarker, Terminal as XTermTerminal } from '@xterm/xterm';
import { AccessibilityCommandId } from '../../accessibility/common/accessibilityCommands.js';
import { terminalStrings } from '../common/terminalStrings.js';
import { TerminalIconPicker } from './terminalIconPicker.js';
import { TerminalResizeDebouncer } from './terminalResizeDebouncer.js';
import { openContextMenu } from './terminalContextMenu.js';
import type { IMenu } from '../../../../platform/actions/common/actions.js';
import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js';
import { TerminalContribCommandId } from '../terminalContribExports.js';
import type { IProgressState } from '@xterm/addon-progress';
const enum Constants {
/**
* The maximum amount of milliseconds to wait for a container before starting to create the
* terminal process. This period helps ensure the terminal has good initial dimensions to work
* with if it's going to be a foreground terminal.
*/
WaitForContainerThreshold = 100,
DefaultCols = 80,
DefaultRows = 30,
MaxCanvasWidth = 4096
}
let xtermConstructor: Promise<typeof XTermTerminal> | undefined;
interface ICanvasDimensions {
width: number;
height: number;
}
interface IGridDimensions {
cols: number;
rows: number;
}
const shellIntegrationSupportedShellTypes: (PosixShellType | GeneralShellType | WindowsShellType)[] = [
PosixShellType.Bash,
PosixShellType.Zsh,
GeneralShellType.PowerShell,
GeneralShellType.Python,
];
export class TerminalInstance extends Disposable implements ITerminalInstance {
private static _lastKnownCanvasDimensions: ICanvasDimensions | undefined;
private static _lastKnownGridDimensions: IGridDimensions | undefined;
private static _instanceIdCounter = 1;
private readonly _scopedInstantiationService: IInstantiationService;
private readonly _processManager: ITerminalProcessManager;
private readonly _contributions: Map<string, ITerminalContribution> = new Map();
private readonly _resource: URI;
/**
* Resolves when xterm.js is ready, this will be undefined if the terminal instance is disposed
* before xterm.js could be created.
*/
private _xtermReadyPromise: Promise<XtermTerminal | undefined>;
private _pressAnyKeyToCloseListener: IDisposable | undefined;
private _instanceId: number;
private _latestXtermWriteData: number = 0;
private _latestXtermParseData: number = 0;
private _isExiting: boolean;
private _hadFocusOnExit: boolean;
private _isVisible: boolean;
private _exitCode: number | undefined;
private _exitReason: TerminalExitReason | undefined;
private _skipTerminalCommands: string[];
private _shellType: TerminalShellType | undefined;
private _title: string = '';
private _titleSource: TitleEventSource = TitleEventSource.Process;
private _container: HTMLElement | undefined;
private _wrapperElement: (HTMLElement & { xterm?: XTermTerminal });
get domElement(): HTMLElement { return this._wrapperElement; }
private _horizontalScrollbar: DomScrollableElement | undefined;
private _terminalFocusContextKey: IContextKey<boolean>;
private _terminalHasFixedWidth: IContextKey<boolean>;
private _terminalHasTextContextKey: IContextKey<boolean>;
private _terminalAltBufferActiveContextKey: IContextKey<boolean>;
private _terminalShellIntegrationEnabledContextKey: IContextKey<boolean>;
private _cols: number = 0;
private _rows: number = 0;
private _fixedCols: number | undefined;
private _fixedRows: number | undefined;
private _cwd: string | undefined = undefined;
private _initialCwd: string | undefined = undefined;
private _injectedArgs: string[] | undefined = undefined;
private _layoutSettingsChanged: boolean = true;
private _dimensionsOverride: ITerminalDimensionsOverride | undefined;
private _areLinksReady: boolean = false;
private readonly _initialDataEventsListener: MutableDisposable<IDisposable> = this._register(new MutableDisposable());
private _initialDataEvents: string[] | undefined = [];
private _containerReadyBarrier: AutoOpenBarrier;
private _attachBarrier: AutoOpenBarrier;
private _icon: TerminalIcon | undefined;
private readonly _messageTitleDisposable: MutableDisposable<IDisposable> = this._register(new MutableDisposable());
private _widgetManager: TerminalWidgetManager;
private readonly _dndObserver: MutableDisposable<IDisposable> = this._register(new MutableDisposable());
private _lastLayoutDimensions: dom.Dimension | undefined;
private _hasHadInput: boolean;
private _description?: string;
private _processName: string = '';
private _sequence?: string;
private _staticTitle?: string;
private _workspaceFolder?: IWorkspaceFolder;
private _labelComputer?: TerminalLabelComputer;
private _userHome?: string;
private _hasScrollBar?: boolean;
private _usedShellIntegrationInjection: boolean = false;
get usedShellIntegrationInjection(): boolean { return this._usedShellIntegrationInjection; }
private _lineDataEventAddon: LineDataEventAddon | undefined;
private readonly _scopedContextKeyService: IContextKeyService;
private _resizeDebouncer?: TerminalResizeDebouncer;
private _pauseInputEventBarrier: Barrier | undefined;
pauseInputEvents(barrier: Barrier): void {
this._pauseInputEventBarrier = barrier;
}
readonly capabilities = this._register(new TerminalCapabilityStoreMultiplexer());
readonly statusList: ITerminalStatusList;
get store(): DisposableStore {
return this._store;
}
get extEnvironmentVariableCollection(): IMergedEnvironmentVariableCollection | undefined { return this._processManager.extEnvironmentVariableCollection; }
xterm?: XtermTerminal;
disableLayout: boolean = false;
get waitOnExit(): ITerminalInstance['waitOnExit'] { return this._shellLaunchConfig.attachPersistentProcess?.waitOnExit || this._shellLaunchConfig.waitOnExit; }
set waitOnExit(value: ITerminalInstance['waitOnExit']) {
this._shellLaunchConfig.waitOnExit = value;
}
private _targetRef: ImmortalReference<TerminalLocation | undefined> = new ImmortalReference(undefined);
get targetRef(): IReference<TerminalLocation | undefined> { return this._targetRef; }
get target(): TerminalLocation | undefined { return this._targetRef.object; }
set target(value: TerminalLocation | undefined) {
this._targetRef.object = value;
this._onDidChangeTarget.fire(value);
}
get instanceId(): number { return this._instanceId; }
get resource(): URI { return this._resource; }
get cols(): number {
if (this._fixedCols !== undefined) {
return this._fixedCols;
}
if (this._dimensionsOverride && this._dimensionsOverride.cols) {
if (this._dimensionsOverride.forceExactSize) {
return this._dimensionsOverride.cols;
}
return Math.min(Math.max(this._dimensionsOverride.cols, 2), this._cols);
}
return this._cols;
}
get rows(): number {
if (this._fixedRows !== undefined) {
return this._fixedRows;
}
if (this._dimensionsOverride && this._dimensionsOverride.rows) {
if (this._dimensionsOverride.forceExactSize) {
return this._dimensionsOverride.rows;
}
return Math.min(Math.max(this._dimensionsOverride.rows, 2), this._rows);
}
return this._rows;
}
get isDisposed(): boolean { return this._store.isDisposed; }
get fixedCols(): number | undefined { return this._fixedCols; }
get fixedRows(): number | undefined { return this._fixedRows; }
get maxCols(): number { return this._cols; }
get maxRows(): number { return this._rows; }
// TODO: Ideally processId would be merged into processReady
get processId(): number | undefined { return this._processManager.shellProcessId; }
// TODO: How does this work with detached processes?
// TODO: Should this be an event as it can fire twice?
get processReady(): Promise<void> { return this._processManager.ptyProcessReady; }
get hasChildProcesses(): boolean { return this.shellLaunchConfig.attachPersistentProcess?.hasChildProcesses || this._processManager.hasChildProcesses; }
get reconnectionProperties(): IReconnectionProperties | undefined { return this.shellLaunchConfig.attachPersistentProcess?.reconnectionProperties || this.shellLaunchConfig.reconnectionProperties; }
get areLinksReady(): boolean { return this._areLinksReady; }
get initialDataEvents(): string[] | undefined { return this._initialDataEvents; }
get exitCode(): number | undefined { return this._exitCode; }
get exitReason(): TerminalExitReason | undefined { return this._exitReason; }
get hadFocusOnExit(): boolean { return this._hadFocusOnExit; }
get isTitleSetByProcess(): boolean { return !!this._messageTitleDisposable.value; }
get shellLaunchConfig(): IShellLaunchConfig { return this._shellLaunchConfig; }
get shellType(): TerminalShellType | undefined { return this._shellType; }
get os(): OperatingSystem | undefined { return this._processManager.os; }
get isRemote(): boolean { return this._processManager.remoteAuthority !== undefined; }
get remoteAuthority(): string | undefined { return this._processManager.remoteAuthority; }
get hasFocus(): boolean { return dom.isAncestorOfActiveElement(this._wrapperElement); }
get title(): string { return this._title; }
get titleSource(): TitleEventSource { return this._titleSource; }
get icon(): TerminalIcon | undefined { return this._getIcon(); }
get color(): string | undefined { return this._getColor(); }
get processName(): string { return this._processName; }
get sequence(): string | undefined { return this._sequence; }
get staticTitle(): string | undefined { return this._staticTitle; }
get progressState(): IProgressState | undefined { return this.xterm?.progressState; }
get workspaceFolder(): IWorkspaceFolder | undefined { return this._workspaceFolder; }
get cwd(): string | undefined { return this._cwd; }
get initialCwd(): string | undefined { return this._initialCwd; }
get description(): string | undefined {
if (this._description) {
return this._description;
}
const type = this.shellLaunchConfig.attachPersistentProcess?.type || this.shellLaunchConfig.type;
switch (type) {
case 'Task': return terminalStrings.typeTask;
case 'Local': return terminalStrings.typeLocal;
default: return undefined;
}
}
get userHome(): string | undefined { return this._userHome; }
get shellIntegrationNonce(): string { return this._processManager.shellIntegrationNonce; }
get injectedArgs(): string[] | undefined { return this._injectedArgs; }
// The onExit event is special in that it fires and is disposed after the terminal instance
// itself is disposed
private readonly _onExit = new Emitter<number | ITerminalLaunchError | undefined>();
readonly onExit = this._onExit.event;
private readonly _onDisposed = this._register(new Emitter<ITerminalInstance>());
readonly onDisposed = this._onDisposed.event;
private readonly _onProcessIdReady = this._register(new Emitter<ITerminalInstance>());
readonly onProcessIdReady = this._onProcessIdReady.event;
private readonly _onProcessReplayComplete = this._register(new Emitter<void>());
readonly onProcessReplayComplete = this._onProcessReplayComplete.event;
private readonly _onTitleChanged = this._register(new Emitter<ITerminalInstance>());
readonly onTitleChanged = this._onTitleChanged.event;
private readonly _onIconChanged = this._register(new Emitter<{ instance: ITerminalInstance; userInitiated: boolean }>());
readonly onIconChanged = this._onIconChanged.event;
private readonly _onWillData = this._register(new Emitter<string>());
readonly onWillData = this._onWillData.event;
private readonly _onData = this._register(new Emitter<string>());
readonly onData = this._onData.event;
private readonly _onBinary = this._register(new Emitter<string>());
readonly onBinary = this._onBinary.event;
private readonly _onRequestExtHostProcess = this._register(new Emitter<ITerminalInstance>());
readonly onRequestExtHostProcess = this._onRequestExtHostProcess.event;
private readonly _onDimensionsChanged = this._register(new Emitter<void>());
readonly onDimensionsChanged = this._onDimensionsChanged.event;
private readonly _onMaximumDimensionsChanged = this._register(new Emitter<void>());
readonly onMaximumDimensionsChanged = this._onMaximumDimensionsChanged.event;
private readonly _onDidFocus = this._register(new Emitter<ITerminalInstance>());
readonly onDidFocus = this._onDidFocus.event;
private readonly _onDidRequestFocus = this._register(new Emitter<void>());
readonly onDidRequestFocus = this._onDidRequestFocus.event;
private readonly _onDidBlur = this._register(new Emitter<ITerminalInstance>());
readonly onDidBlur = this._onDidBlur.event;
private readonly _onDidInputData = this._register(new Emitter<string>());
readonly onDidInputData = this._onDidInputData.event;
private readonly _onDidChangeSelection = this._register(new Emitter<ITerminalInstance>());
readonly onDidChangeSelection = this._onDidChangeSelection.event;
private readonly _onRequestAddInstanceToGroup = this._register(new Emitter<IRequestAddInstanceToGroupEvent>());
readonly onRequestAddInstanceToGroup = this._onRequestAddInstanceToGroup.event;
private readonly _onDidChangeHasChildProcesses = this._register(new Emitter<boolean>());
readonly onDidChangeHasChildProcesses = this._onDidChangeHasChildProcesses.event;
private readonly _onDidExecuteText = this._register(new Emitter<void>());
readonly onDidExecuteText = this._onDidExecuteText.event;
private readonly _onDidChangeTarget = this._register(new Emitter<TerminalLocation | undefined>());
readonly onDidChangeTarget = this._onDidChangeTarget.event;
private readonly _onDidSendText = this._register(new Emitter<string>());
readonly onDidSendText = this._onDidSendText.event;
private readonly _onDidChangeShellType = this._register(new Emitter<TerminalShellType>());
readonly onDidChangeShellType = this._onDidChangeShellType.event;
private readonly _onDidChangeVisibility = this._register(new Emitter<boolean>());
readonly onDidChangeVisibility = this._onDidChangeVisibility.event;
private readonly _onLineData = this._register(new Emitter<string>({
onDidAddFirstListener: async () => (this.xterm ?? await this._xtermReadyPromise)?.raw.loadAddon(this._lineDataEventAddon!)
}));
readonly onLineData = this._onLineData.event;
constructor(
private readonly _terminalShellTypeContextKey: IContextKey<string>,
private _shellLaunchConfig: IShellLaunchConfig,
@IContextKeyService private readonly _contextKeyService: IContextKeyService,
@IContextMenuService private readonly _contextMenuService: IContextMenuService,
@IInstantiationService instantiationService: IInstantiationService,
@ITerminalConfigurationService private readonly _terminalConfigurationService: ITerminalConfigurationService,
@ITerminalProfileResolverService private readonly _terminalProfileResolverService: ITerminalProfileResolverService,
@IPathService private readonly _pathService: IPathService,
@IKeybindingService private readonly _keybindingService: IKeybindingService,
@INotificationService private readonly _notificationService: INotificationService,
@IPreferencesService private readonly _preferencesService: IPreferencesService,
@IViewsService private readonly _viewsService: IViewsService,
@IThemeService private readonly _themeService: IThemeService,
@IConfigurationService private readonly _configurationService: IConfigurationService,
@ITerminalLogService private readonly _logService: ITerminalLogService,
@IStorageService private readonly _storageService: IStorageService,
@IAccessibilityService private readonly _accessibilityService: IAccessibilityService,
@IProductService private readonly _productService: IProductService,
@IQuickInputService private readonly _quickInputService: IQuickInputService,
@IWorkbenchEnvironmentService workbenchEnvironmentService: IWorkbenchEnvironmentService,
@IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService,
@IEditorService private readonly _editorService: IEditorService,
@IWorkspaceTrustRequestService private readonly _workspaceTrustRequestService: IWorkspaceTrustRequestService,
@IHistoryService private readonly _historyService: IHistoryService,
@ITelemetryService private readonly _telemetryService: ITelemetryService,
@IOpenerService private readonly _openerService: IOpenerService,
@ICommandService private readonly _commandService: ICommandService,
@IAccessibilitySignalService private readonly _accessibilitySignalService: IAccessibilitySignalService,
@IViewDescriptorService private readonly _viewDescriptorService: IViewDescriptorService,
) {
super();
this._wrapperElement = document.createElement('div');
this._wrapperElement.classList.add('terminal-wrapper');
this._widgetManager = this._register(instantiationService.createInstance(TerminalWidgetManager));
this._skipTerminalCommands = [];
this._isExiting = false;
this._hadFocusOnExit = false;
this._isVisible = false;
this._instanceId = TerminalInstance._instanceIdCounter++;
this._hasHadInput = false;
this._fixedRows = _shellLaunchConfig.attachPersistentProcess?.fixedDimensions?.rows;
this._fixedCols = _shellLaunchConfig.attachPersistentProcess?.fixedDimensions?.cols;
this._resource = getTerminalUri(this._workspaceContextService.getWorkspace().id, this.instanceId, this.title);
if (this._shellLaunchConfig.attachPersistentProcess?.hideFromUser) {
this._shellLaunchConfig.hideFromUser = this._shellLaunchConfig.attachPersistentProcess.hideFromUser;
}
if (this._shellLaunchConfig.attachPersistentProcess?.isFeatureTerminal) {
this._shellLaunchConfig.isFeatureTerminal = this._shellLaunchConfig.attachPersistentProcess.isFeatureTerminal;
}
if (this._shellLaunchConfig.attachPersistentProcess?.type) {
this._shellLaunchConfig.type = this._shellLaunchConfig.attachPersistentProcess.type;
}
if (this.shellLaunchConfig.cwd) {
const cwdUri = typeof this._shellLaunchConfig.cwd === 'string' ? URI.from({
scheme: Schemas.file,
path: this._shellLaunchConfig.cwd
}) : this._shellLaunchConfig.cwd;
if (cwdUri) {
this._workspaceFolder = this._workspaceContextService.getWorkspaceFolder(cwdUri) ?? undefined;
}
}
if (!this._workspaceFolder) {
const activeWorkspaceRootUri = this._historyService.getLastActiveWorkspaceRoot();
this._workspaceFolder = activeWorkspaceRootUri ? this._workspaceContextService.getWorkspaceFolder(activeWorkspaceRootUri) ?? undefined : undefined;
}
const scopedContextKeyService = this._register(_contextKeyService.createScoped(this._wrapperElement));
this._scopedContextKeyService = scopedContextKeyService;
this._scopedInstantiationService = this._register(instantiationService.createChild(new ServiceCollection(
[IContextKeyService, scopedContextKeyService]
)));
this._terminalFocusContextKey = TerminalContextKeys.focus.bindTo(scopedContextKeyService);
this._terminalHasFixedWidth = TerminalContextKeys.terminalHasFixedWidth.bindTo(scopedContextKeyService);
this._terminalHasTextContextKey = TerminalContextKeys.textSelected.bindTo(scopedContextKeyService);
this._terminalAltBufferActiveContextKey = TerminalContextKeys.altBufferActive.bindTo(scopedContextKeyService);
this._terminalShellIntegrationEnabledContextKey = TerminalContextKeys.terminalShellIntegrationEnabled.bindTo(scopedContextKeyService);
this._logService.trace(`terminalInstance#ctor (instanceId: ${this.instanceId})`, this._shellLaunchConfig);
this._register(this.capabilities.onDidAddCapabilityType(e => this._logService.debug('terminalInstance added capability', e)));
this._register(this.capabilities.onDidRemoveCapabilityType(e => this._logService.debug('terminalInstance removed capability', e)));
const capabilityListeners = this._register(new DisposableMap<TerminalCapability, IDisposable>());
this._register(this.capabilities.onDidAddCapabilityType(capability => {
capabilityListeners.get(capability)?.dispose();
if (capability === TerminalCapability.CwdDetection) {
const cwdDetection = this.capabilities.get(capability);
if (cwdDetection) {
capabilityListeners.set(capability, cwdDetection.onDidChangeCwd(e => {
this._cwd = e;
this._setTitle(this.title, TitleEventSource.Config);
}));
}
}
if (capability === TerminalCapability.CommandDetection) {
const commandDetection = this.capabilities.get(capability);
if (commandDetection) {
capabilityListeners.set(capability, Event.any(
commandDetection.promptInputModel.onDidStartInput,
commandDetection.promptInputModel.onDidChangeInput,
commandDetection.promptInputModel.onDidFinishInput
)(() => this._labelComputer?.refreshLabel(this)));
}
}
}));
this._register(this.capabilities.onDidRemoveCapabilityType(capability => {
capabilityListeners.get(capability)?.dispose();
}));
// Resolve just the icon ahead of time so that it shows up immediately in the tabs. This is
// disabled in remote because this needs to be sync and the OS may differ on the remote
// which would result in the wrong profile being selected and the wrong icon being
// permanently attached to the terminal. This also doesn't work when the default profile
// setting is set to null, that's handled after the process is created.
if (!this.shellLaunchConfig.executable && !workbenchEnvironmentService.remoteAuthority) {
this._terminalProfileResolverService.resolveIcon(this._shellLaunchConfig, OS);
}
this._icon = _shellLaunchConfig.attachPersistentProcess?.icon || _shellLaunchConfig.icon;
// When a custom pty is used set the name immediately so it gets passed over to the exthost
// and is available when Pseudoterminal.open fires.
if (this.shellLaunchConfig.customPtyImplementation) {
this._setTitle(this._shellLaunchConfig.name, TitleEventSource.Api);
}
this.statusList = this._register(this._scopedInstantiationService.createInstance(TerminalStatusList));
this._initDimensions();
this._processManager = this._createProcessManager();
this._containerReadyBarrier = new AutoOpenBarrier(Constants.WaitForContainerThreshold);
this._attachBarrier = new AutoOpenBarrier(1000);
this._xtermReadyPromise = this._createXterm();
this._xtermReadyPromise.then(async () => {
// Wait for a period to allow a container to be ready
await this._containerReadyBarrier.wait();
// Resolve the executable ahead of time if shell integration is enabled, this should not
// be done for custom PTYs as that would cause extension Pseudoterminal-based terminals
// to hang in resolver extensions
let os: OperatingSystem | undefined;
if (!this.shellLaunchConfig.customPtyImplementation && this._terminalConfigurationService.config.shellIntegration?.enabled && !this.shellLaunchConfig.executable) {
os = await this._processManager.getBackendOS();
const defaultProfile = (await this._terminalProfileResolverService.getDefaultProfile({ remoteAuthority: this.remoteAuthority, os }));
this.shellLaunchConfig.executable = defaultProfile.path;
this.shellLaunchConfig.args = defaultProfile.args;
if (this.shellLaunchConfig.isExtensionOwnedTerminal) {
// Only use default icon and color and env if they are undefined in the SLC
this.shellLaunchConfig.icon ??= defaultProfile.icon;
this.shellLaunchConfig.color ??= defaultProfile.color;
this.shellLaunchConfig.env ??= defaultProfile.env;
} else {
this.shellLaunchConfig.icon = defaultProfile.icon;
this.shellLaunchConfig.color = defaultProfile.color;
this.shellLaunchConfig.env = defaultProfile.env;
}
}
// Resolve the shell type ahead of time to allow features that depend upon it to work
// before the process is actually created (like terminal suggest manual request)
if (os && this.shellLaunchConfig.executable) {
this.setShellType(guessShellTypeFromExecutable(os, this.shellLaunchConfig.executable));
}
await this._createProcess();
// Re-establish the title after reconnect
if (this.shellLaunchConfig.attachPersistentProcess) {
this._cwd = this.shellLaunchConfig.attachPersistentProcess.cwd;
this._setTitle(this.shellLaunchConfig.attachPersistentProcess.title, this.shellLaunchConfig.attachPersistentProcess.titleSource);
this.setShellType(this.shellType);
}
if (this._fixedCols) {
await this._addScrollbar();
}
}).catch((err) => {
// Ignore exceptions if the terminal is already disposed
if (!this.isDisposed) {
throw err;
}
});
this._register(this._configurationService.onDidChangeConfiguration(async e => {
if (e.affectsConfiguration(AccessibilityVerbositySettingId.Terminal)) {
this._setAriaLabel(this.xterm?.raw, this._instanceId, this.title);
}
if (e.affectsConfiguration('terminal.integrated')) {
this.updateConfig();
this.setVisible(this._isVisible);
}
const layoutSettings: string[] = [
TerminalSettingId.FontSize,
TerminalSettingId.FontFamily,
TerminalSettingId.FontWeight,
TerminalSettingId.FontWeightBold,
TerminalSettingId.LetterSpacing,
TerminalSettingId.LineHeight,
'editor.fontFamily'
];
if (layoutSettings.some(id => e.affectsConfiguration(id))) {
this._layoutSettingsChanged = true;
await this._resize();
}
if (e.affectsConfiguration(TerminalSettingId.UnicodeVersion)) {
this._updateUnicodeVersion();
}
if (e.affectsConfiguration('editor.accessibilitySupport')) {
this.updateAccessibilitySupport();
}
if (
e.affectsConfiguration(TerminalSettingId.TerminalTitle) ||
e.affectsConfiguration(TerminalSettingId.TerminalTitleSeparator) ||
e.affectsConfiguration(TerminalSettingId.TerminalDescription)) {
this._labelComputer?.refreshLabel(this);
}
}));
this._register(this._workspaceContextService.onDidChangeWorkspaceFolders(() => this._labelComputer?.refreshLabel(this)));
// Clear out initial data events after 10 seconds, hopefully extension hosts are up and
// running at that point.
let initialDataEventsTimeout: number | undefined = dom.getWindow(this._container).setTimeout(() => {
initialDataEventsTimeout = undefined;
this._initialDataEvents = undefined;
this._initialDataEventsListener.clear();
}, 10000);
this._register(toDisposable(() => {
if (initialDataEventsTimeout) {
dom.getWindow(this._container).clearTimeout(initialDataEventsTimeout);
}
}));
// Initialize contributions
const contributionDescs = TerminalExtensionsRegistry.getTerminalContributions();
for (const desc of contributionDescs) {
if (this._contributions.has(desc.id)) {
onUnexpectedError(new Error(`Cannot have two terminal contributions with the same id ${desc.id}`));
continue;
}
let contribution: ITerminalContribution;
try {
contribution = this._register(this._scopedInstantiationService.createInstance(desc.ctor, {
instance: this,
processManager: this._processManager,
widgetManager: this._widgetManager
}));
this._contributions.set(desc.id, contribution);
} catch (err) {
onUnexpectedError(err);
}
this._xtermReadyPromise.then(xterm => {
if (xterm) {
contribution.xtermReady?.(xterm);
}
});
this._register(this.onDisposed(() => {
contribution.dispose();
this._contributions.delete(desc.id);
// Just in case to prevent potential future memory leaks due to cyclic dependency.
if ('instance' in contribution) {
delete contribution.instance;
}
if ('_instance' in contribution) {
delete contribution._instance;
}
}));
}
}
public getContribution<T extends ITerminalContribution>(id: string): T | null {
return this._contributions.get(id) as T | null;
}
private _getIcon(): TerminalIcon | undefined {
if (!this._icon) {
this._icon = this._processManager.processState >= ProcessState.Launching
? getIconRegistry().getIcon(this._configurationService.getValue(TerminalSettingId.TabsDefaultIcon))
: undefined;
}
return this._icon;
}
private _getColor(): string | undefined {
if (this.shellLaunchConfig.color) {
return this.shellLaunchConfig.color;
}
if (this.shellLaunchConfig?.attachPersistentProcess?.color) {
return this.shellLaunchConfig.attachPersistentProcess.color;
}
if (this._processManager.processState >= ProcessState.Launching) {
return undefined;
}
return undefined;
}
private _initDimensions(): void {
// The terminal panel needs to have been created to get the real view dimensions
if (!this._container) {
// Set the fallback dimensions if not
this._cols = Constants.DefaultCols;
this._rows = Constants.DefaultRows;
return;
}
const computedStyle = dom.getWindow(this._container).getComputedStyle(this._container);
const width = parseInt(computedStyle.width);
const height = parseInt(computedStyle.height);
this._evaluateColsAndRows(width, height);
}
/**
* Evaluates and sets the cols and rows of the terminal if possible.
* @param width The width of the container.
* @param height The height of the container.
* @return The terminal's width if it requires a layout.
*/
private _evaluateColsAndRows(width: number, height: number): number | null {
// Ignore if dimensions are undefined or 0
if (!width || !height) {
this._setLastKnownColsAndRows();
return null;
}
const dimension = this._getDimension(width, height);
if (!dimension) {
this._setLastKnownColsAndRows();
return null;
}
const font = this.xterm ? this.xterm.getFont() : this._terminalConfigurationService.getFont(dom.getWindow(this.domElement));
const newRC = getXtermScaledDimensions(dom.getWindow(this.domElement), font, dimension.width, dimension.height);
if (!newRC) {
this._setLastKnownColsAndRows();
return null;
}
if (this._cols !== newRC.cols || this._rows !== newRC.rows) {
this._cols = newRC.cols;
this._rows = newRC.rows;
this._fireMaximumDimensionsChanged();
}
return dimension.width;
}
private _setLastKnownColsAndRows(): void {
if (TerminalInstance._lastKnownGridDimensions) {
this._cols = TerminalInstance._lastKnownGridDimensions.cols;
this._rows = TerminalInstance._lastKnownGridDimensions.rows;
}
}
@debounce(50)
private _fireMaximumDimensionsChanged(): void {
this._onMaximumDimensionsChanged.fire();
}
private _getDimension(width: number, height: number): ICanvasDimensions | undefined {
// The font needs to have been initialized
const font = this.xterm ? this.xterm.getFont() : this._terminalConfigurationService.getFont(dom.getWindow(this.domElement));
if (!font || !font.charWidth || !font.charHeight) {
return undefined;
}
if (!this.xterm?.raw.element) {
return undefined;
}
const computedStyle = dom.getWindow(this.xterm.raw.element).getComputedStyle(this.xterm.raw.element);
const horizontalPadding = parseInt(computedStyle.paddingLeft) + parseInt(computedStyle.paddingRight) + 14/*scroll bar padding*/;
const verticalPadding = parseInt(computedStyle.paddingTop) + parseInt(computedStyle.paddingBottom);
TerminalInstance._lastKnownCanvasDimensions = new dom.Dimension(
Math.min(Constants.MaxCanvasWidth, width - horizontalPadding),
height - verticalPadding + (this._hasScrollBar && this._horizontalScrollbar ? -5/* scroll bar height */ : 0));
return TerminalInstance._lastKnownCanvasDimensions;
}
get persistentProcessId(): number | undefined { return this._processManager.persistentProcessId; }
get shouldPersist(): boolean { return this._processManager.shouldPersist && !this.shellLaunchConfig.isTransient && (!this.reconnectionProperties || this._configurationService.getValue('task.reconnection') === true); }
public static getXtermConstructor(keybindingService: IKeybindingService, contextKeyService: IContextKeyService) {
const keybinding = keybindingService.lookupKeybinding(TerminalContribCommandId.A11yFocusAccessibleBuffer, contextKeyService);
if (xtermConstructor) {
return xtermConstructor;
}
xtermConstructor = Promises.withAsyncBody<typeof XTermTerminal>(async (resolve) => {
const Terminal = (await importAMDNodeModule<typeof import('@xterm/xterm')>('@xterm/xterm', 'lib/xterm.js')).Terminal;
// Localize strings
Terminal.strings.promptLabel = nls.localize('terminal.integrated.a11yPromptLabel', 'Terminal input');
Terminal.strings.tooMuchOutput = keybinding ? nls.localize('terminal.integrated.useAccessibleBuffer', 'Use the accessible buffer {0} to manually review output', keybinding.getLabel()) : nls.localize('terminal.integrated.useAccessibleBufferNoKb', 'Use the Terminal: Focus Accessible Buffer command to manually review output');
resolve(Terminal);
});
return xtermConstructor;
}
/**
* Create xterm.js instance and attach data listeners.
*/
protected async _createXterm(): Promise<XtermTerminal | undefined> {
const Terminal = await TerminalInstance.getXtermConstructor(this._keybindingService, this._contextKeyService);
if (this.isDisposed) {
return undefined;
}
const disableShellIntegrationReporting = (this.shellLaunchConfig.executable === undefined || this.shellType === undefined) || !shellIntegrationSupportedShellTypes.includes(this.shellType);
const xterm = this._scopedInstantiationService.createInstance(XtermTerminal, Terminal, {
cols: this._cols,
rows: this._rows,
xtermColorProvider: this._scopedInstantiationService.createInstance(TerminalInstanceColorProvider, this._targetRef),
capabilities: this.capabilities,
shellIntegrationNonce: this._processManager.shellIntegrationNonce,
disableShellIntegrationReporting,
});
this.xterm = xterm;
this._resizeDebouncer = this._register(new TerminalResizeDebouncer(
() => this._isVisible,
() => xterm,
async (cols, rows) => {
xterm.raw.resize(cols, rows);
await this._updatePtyDimensions(xterm.raw);
},
async (cols) => {
xterm.raw.resize(cols, xterm.raw.rows);
await this._updatePtyDimensions(xterm.raw);
},
async (rows) => {
xterm.raw.resize(xterm.raw.cols, rows);
await this._updatePtyDimensions(xterm.raw);
}
));
this._register(toDisposable(() => this._resizeDebouncer = undefined));
this.updateAccessibilitySupport();
this._register(this.xterm.onDidRequestRunCommand(e => {
this.sendText(e.command.command, e.noNewLine ? false : true);
}));
this._register(this.xterm.onDidRequestRefreshDimensions(() => {
if (this._lastLayoutDimensions) {
this.layout(this._lastLayoutDimensions);
}
}));
// Write initial text, deferring onLineFeed listener when applicable to avoid firing
// onLineData events containing initialText
const initialTextWrittenPromise = this._shellLaunchConfig.initialText ? new Promise<void>(r => this._writeInitialText(xterm, r)) : undefined;
const lineDataEventAddon = this._register(new LineDataEventAddon(initialTextWrittenPromise));
this._register(lineDataEventAddon.onLineData(e => this._onLineData.fire(e)));
this._lineDataEventAddon = lineDataEventAddon;
// Delay the creation of the bell listener to avoid showing the bell when the terminal
// starts up or reconnects
disposableTimeout(() => {
this._register(xterm.raw.onBell(() => {
if (this._configurationService.getValue(TerminalSettingId.EnableBell) || this._configurationService.getValue(TerminalSettingId.EnableVisualBell)) {
this.statusList.add({
id: TerminalStatus.Bell,
severity: Severity.Warning,
icon: Codicon.bell,
tooltip: nls.localize('bellStatus', "Bell")
}, this._terminalConfigurationService.config.bellDuration);
}
this._accessibilitySignalService.playSignal(AccessibilitySignal.terminalBell);
}));
}, 1000, this._store);
this._register(xterm.raw.onSelectionChange(() => this._onDidChangeSelection.fire(this)));
this._register(xterm.raw.buffer.onBufferChange(() => this._refreshAltBufferContextKey()));
this._register(this._processManager.onProcessData(e => this._onProcessData(e)));
this._register(xterm.raw.onData(async data => {
await this._pauseInputEventBarrier?.wait();
await this._processManager.write(data);
this._onDidInputData.fire(data);
}));
this._register(xterm.raw.onBinary(data => this._processManager.processBinary(data)));
// Init winpty compat and link handler after process creation as they rely on the
// underlying process OS
this._register(this._processManager.onProcessReady(async (processTraits) => {
if (this._processManager.os) {
lineDataEventAddon.setOperatingSystem(this._processManager.os);
}
xterm.raw.options.windowsPty = processTraits.windowsPty;
}));
this._register(this._processManager.onRestoreCommands(e => this.xterm?.shellIntegration.deserialize(e)));
this._register(this._viewDescriptorService.onDidChangeLocation(({ views }) => {
if (views.some(v => v.id === TERMINAL_VIEW_ID)) {
xterm.refresh();
}
}));
this._register(xterm.onDidChangeProgress(() => this._labelComputer?.refreshLabel(this)));
// Set up updating of the process cwd on key press, this is only needed when the cwd
// detection capability has not been registered
if (!this.capabilities.has(TerminalCapability.CwdDetection)) {
let onKeyListener: IDisposable | undefined = xterm.raw.onKey(e => {
const event = new StandardKeyboardEvent(e.domEvent);
if (event.equals(KeyCode.Enter)) {
this._updateProcessCwd();
}
});
this._register(this.capabilities.onDidAddCapabilityType(e => {
if (e === TerminalCapability.CwdDetection) {
onKeyListener?.dispose();
onKeyListener = undefined;
}
}));
}
this._pathService.userHome().then(userHome => {
this._userHome = userHome.fsPath;
});
if (this._isVisible) {
this._open();
}
return xterm;
}
async runCommand(commandLine: string, shouldExecute: boolean): Promise<void> {
let commandDetection = this.capabilities.get(TerminalCapability.CommandDetection);
// Await command detection if the terminal is starting up
if (!commandDetection && (this._processManager.processState === ProcessState.Uninitialized || this._processManager.processState === ProcessState.Launching)) {
const store = new DisposableStore();
await Promise.race([
new Promise<void>(r => {
store.add(this.capabilities.onDidAddCapabilityType(e => {
if (e === TerminalCapability.CommandDetection) {
commandDetection = this.capabilities.get(TerminalCapability.CommandDetection);
r();
}
}));
}),
timeout(2000),
]);
store.dispose();
}
// Determine whether to send ETX (ctrl+c) before running the command. This should always
// happen unless command detection can reliably say that a command is being entered and
// there is no content in the prompt
if (!commandDetection || commandDetection.promptInputModel.value.length > 0) {
await this.sendText('\x03', false);
// Wait a little before running the command to avoid the sequences being echoed while the ^C
// is being evaluated
await timeout(100);
}
// Use bracketed paste mode only when not running the command
await this.sendText(commandLine, shouldExecute, !shouldExecute);
}
detachFromElement(): void {
this._wrapperElement.remove();
this._container = undefined;
}
attachToElement(container: HTMLElement): void {
// The container did not change, do nothing
if (this._container === container) {
return;
}
if (!this._attachBarrier.isOpen()) {
this._attachBarrier.open();
}
// The container changed, reattach
this._container = container;
this._container.appendChild(this._wrapperElement);
// If xterm is already attached, call open again to pick up any changes to the window.
if (this.xterm?.raw.element) {
this.xterm.raw.open(this.xterm.raw.element);
}
this.xterm?.refresh();
setTimeout(() => {
if (this._store.isDisposed) {
return;
}
this._initDragAndDrop(container);
}, 0);
}
/**
* Opens the the terminal instance inside the parent DOM element previously set with
* `attachToElement`, you must ensure the parent DOM element is explicitly visible before
* invoking this function as it performs some DOM calculations internally
*/
private _open(): void {
if (!this.xterm || this.xterm.raw.element) {
return;
}
if (!this._container || !this._container.isConnected) {
throw new Error('A container element needs to be set with `attachToElement` and be part of the DOM before calling `_open`');
}
const xtermElement = document.createElement('div');
this._wrapperElement.appendChild(xtermElement);
this._container.appendChild(this._wrapperElement);
const xterm = this.xterm;
// Attach the xterm object to the DOM, exposing it to the smoke tests
this._wrapperElement.xterm = xterm.raw;
const screenElement = xterm.attachToElement(xtermElement);
// Fire xtermOpen on all contributions
for (const contribution of this._contributions.values()) {
if (!this.xterm) {
this._xtermReadyPromise.then(xterm => {
if (xterm) {
contribution.xtermOpen?.(xterm);
}
});
} else {
contribution.xtermOpen?.(this.xterm);
}
}
this._register(xterm.shellIntegration.onDidChangeStatus(() => {
if (this.hasFocus) {
this._setShellIntegrationContextKey();
} else {
this._terminalShellIntegrationEnabledContextKey.reset();
}
}));
if (!xterm.raw.element || !xterm.raw.textarea) {
throw new Error('xterm elements not set after open');
}
this._setAriaLabel(xterm.raw, this._instanceId, this._title);
xterm.raw.attachCustomKeyEventHandler((event: KeyboardEvent): boolean => {
// Disable all input if the terminal is exiting
if (this._isExiting) {
return false;
}
const standardKeyboardEvent = new StandardKeyboardEvent(event);
const resolveResult = this._keybindingService.softDispatch(standardKeyboardEvent, standardKeyboardEvent.target);
// Respect chords if the allowChords setting is set and it's not Escape. Escape is
// handled specially for Zen Mode's Escape, Escape chord, plus it's important in
// terminals generally
const isValidChord = resolveResult.kind === ResultKind.MoreChordsNeeded && this._terminalConfigurationService.config.allowChords && event.key !== 'Escape';
if (this._keybindingService.inChordMode || isValidChord) {
event.preventDefault();
return false;
}
const SHOW_TERMINAL_CONFIG_PROMPT_KEY = 'terminal.integrated.showTerminalConfigPrompt';
const EXCLUDED_KEYS = ['RightArrow', 'LeftArrow', 'UpArrow', 'DownArrow', 'Space', 'Meta', 'Control', 'Shift', 'Alt', '', 'Delete', 'Backspace', 'Tab'];
// only keep track of input if prompt hasn't already been shown
if (this._storageService.getBoolean(SHOW_TERMINAL_CONFIG_PROMPT_KEY, StorageScope.APPLICATION, true) &&
!EXCLUDED_KEYS.includes(event.key) &&
!event.ctrlKey &&
!event.shiftKey &&
!event.altKey) {
this._hasHadInput = true;
}
// for keyboard events that resolve to commands described
// within commandsToSkipShell, either alert or skip processing by xterm.js
if (resolveResult.kind === ResultKind.KbFound && resolveResult.commandId && this._skipTerminalCommands.some(k => k === resolveResult.commandId) && !this._terminalConfigurationService.config.sendKeybindingsToShell) {
// don't alert when terminal is opened or closed
if (this._storageService.getBoolean(SHOW_TERMINAL_CONFIG_PROMPT_KEY, StorageScope.APPLICATION, true) &&
this._hasHadInput &&
!TERMINAL_CREATION_COMMANDS.includes(resolveResult.commandId)) {
this._notificationService.prompt(
Severity.Info,
nls.localize('keybindingHandling', "Some keybindings don't go to the terminal by default and are handled by {0} instead.", this._productService.nameLong),
[
{
label: nls.localize('configureTerminalSettings', "Configure Terminal Settings"),
run: () => {
this._preferencesService.openSettings({ jsonEditor: false, query: `@id:${TerminalSettingId.CommandsToSkipShell},${TerminalSettingId.SendKeybindingsToShell},${TerminalSettingId.AllowChords}` });
}
} satisfies IPromptChoice
]
);
this._storageService.store(SHOW_TERMINAL_CONFIG_PROMPT_KEY, false, StorageScope.APPLICATION, StorageTarget.USER);
}
event.preventDefault();
return false;
}
// Skip processing by xterm.js of keyboard events that match menu bar mnemonics
if (this._terminalConfigurationService.config.allowMnemonics && !isMacintosh && event.altKey) {
return false;
}
// If tab focus mode is on, tab is not passed to the terminal
if (TabFocus.getTabFocusMode() && event.key === 'Tab') {
return false;
}
// Prevent default when shift+tab is being sent to the terminal to avoid it bubbling up
// and changing focus https://github.com/microsoft/vscode/issues/188329
if (event.key === 'Tab' && event.shiftKey) {
event.preventDefault();
return true;
}
// Always have alt+F4 skip the terminal on Windows and allow it to be handled by the
// system
if (isWindows && event.altKey && event.key === 'F4' && !event.ctrlKey) {
return false;
}
// Fallback to force ctrl+v to paste on browsers that do not support
// navigator.clipboard.readText
if (!BrowserFeatures.clipboard.readText && event.key === 'v' && event.ctrlKey) {
return false;
}
return true;
});
this._register(dom.addDisposableListener(xterm.raw.element, 'mousedown', () => {
// We need to listen to the mouseup event on the document since the user may release
// the mouse button anywhere outside of _xterm.element.
const listener = dom.addDisposableListener(xterm.raw.element!.ownerDocument, 'mouseup', () => {
// Delay with a setTimeout to allow the mouseup to propagate through the DOM
// before evaluating the new selection state.
setTimeout(() => this._refreshSelectionContextKey(), 0);
listener.dispose();
});
}));
this._register(dom.addDisposableListener(xterm.raw.element, 'touchstart', () => {
xterm.raw.focus();
}));
// xterm.js currently drops selection on keyup as we need to handle this case.
this._register(dom.addDisposableListener(xterm.raw.element, 'keyup', () => {
// Wait until keyup has propagated through the DOM before evaluating
// the new selection state.
setTimeout(() => this._refreshSelectionContextKey(), 0);
}));
this._register(dom.addDisposableListener(xterm.raw.textarea, 'focus', () => this._setFocus(true)));
this._register(dom.addDisposableListener(xterm.raw.textarea, 'blur', () => this._setFocus(false)));
this._register(dom.addDisposableListener(xterm.raw.textarea, 'focusout', () => this._setFocus(false)));
this._initDragAndDrop(this._container);
this._widgetManager.attachToElement(screenElement);
if (this._lastLayoutDimensions) {
this.layout(this._lastLayoutDimensions);
}
this.updateConfig();
// If IShellLaunchConfig.waitOnExit was true and the process finished before the terminal
// panel was initialized.
if (xterm.raw.options.disableStdin) {
this._attachPressAnyKeyToCloseListener(xterm.raw);
}
}
private _setFocus(focused?: boolean): void {
if (focused) {
this._terminalFocusContextKey.set(true);
this._setShellIntegrationContextKey();
this._onDidFocus.fire(this);
} else {
this.resetFocusContextKey();
this._onDidBlur.fire(this);
this._refreshSelectionContextKey();
}
}
private _setShellIntegrationContextKey(): void {
if (this.xterm) {
this._terminalShellIntegrationEnabledContextKey.set(this.xterm.shellIntegration.status === ShellIntegrationStatus.VSCode);
}
}
resetFocusContextKey(): void {
this._terminalFocusContextKey.reset();
this._terminalShellIntegrationEnabledContextKey.reset();
}
private _initDragAndDrop(container: HTMLElement) {
const store = new DisposableStore();
const dndController = store.add(this._scopedInstantiationService.createInstance(TerminalInstanceDragAndDropController, container));
store.add(dndController.onDropTerminal(e => this._onRequestAddInstanceToGroup.fire(e)));
store.add(dndController.onDropFile(async path => {
this.focus();
await this.sendPath(path, false);
}));
store.add(new dom.DragAndDropObserver(container, dndController));
this._dndObserver.value = store;
}
hasSelection(): boolean {
return this.xterm ? this.xterm.raw.hasSelection() : false;
}
get selection(): string | undefined {
return this.xterm && this.hasSelection() ? this.xterm.raw.getSelection() : undefined;
}
clearSelection(): void {
this.xterm?.raw.clearSelection();
}
private _refreshAltBufferContextKey() {
this._terminalAltBufferActiveContextKey.set(!!(this.xterm && this.xterm.raw.buffer.active === this.xterm.raw.buffer.alternate));
}
override dispose(reason?: TerminalExitReason): void {
if (this.shellLaunchConfig.type === 'Task' && reason === TerminalExitReason.Process && this._exitCode !== 0 && !this.shellLaunchConfig.waitOnExit) {
return;
}
if (this.isDisposed) {
return;
}
this._logService.trace(`terminalInstance#dispose (instanceId: ${this.instanceId})`);
dispose(this._widgetManager);
if (this.xterm?.raw.element) {
this._hadFocusOnExit = this.hasFocus;
}
if (this._wrapperElement.xterm) {
this._wrapperElement.xterm = undefined;
}
if (this._horizontalScrollbar) {
this._horizontalScrollbar.dispose();
this._horizontalScrollbar = undefined;
}
try {
this.xterm?.dispose();
} catch (err: unknown) {
// See https://github.com/microsoft/vscode/issues/153486
this._logService.error('Exception occurred during xterm disposal', err);
}
// HACK: Workaround for Firefox bug https://bugzilla.mozilla.org/show_bug.cgi?id=559561,
// as 'blur' event in xterm.raw.textarea is not triggered on xterm.dispose()
// See https://github.com/microsoft/vscode/issues/138358
if (isFirefox) {
this.resetFocusContextKey();
this._terminalHasTextContextKey.reset();
this._onDidBlur.fire(this);
}
if (this._pressAnyKeyToCloseListener) {
this._pressAnyKeyToCloseListener.dispose();
this._pressAnyKeyToCloseListener = undefined;
}
if (this._exitReason === undefined) {
this._exitReason = reason ?? TerminalExitReason.Unknown;
}
this._processManager.dispose();
// Process manager dispose/shutdown doesn't fire process exit, trigger with undefined if it
// hasn't happened yet
this._onProcessExit(undefined);
this._onDisposed.fire(this);
super.dispose();
}
async detachProcessAndDispose(reason: TerminalExitReason): Promise<void> {
// Detach the process and dispose the instance, without the instance dispose the terminal
// won't go away. Force persist if the detach was requested by the user (not shutdown).
await this._processManager.detachFromProcess(reason === TerminalExitReason.User);
this.dispose(reason);
}
focus(force?: boolean): void {
this._refreshAltBufferContextKey();
if (!this.xterm) {
return;
}
if (force || !dom.getActiveWindow().getSelection()?.toString()) {
this.xterm.raw.focus();
this._onDidRequestFocus.fire();
}
}
async focusWhenReady(force?: boolean): Promise<void> {
await this._xtermReadyPromise;
await this._attachBarrier.wait();
this.focus(force);
}
async sendText(text: string, shouldExecute: boolean, bracketedPasteMode?: boolean): Promise<void> {
// Apply bracketed paste sequences if the terminal has the mode enabled, this will prevent
// the text from triggering keybindings and ensure new lines are handled properly
if (bracketedPasteMode && this.xterm?.raw.modes.bracketedPasteMode) {
text = `\x1b[200~${text}\x1b[201~`;
}
// Normalize line endings to 'enter' press.
text = text.replace(/\r?\n/g, '\r');
if (shouldExecute && !text.endsWith('\r')) {
text += '\r';
}
// Send it to the process
this._logService.debug('sending data (vscode)', text);
await this._processManager.write(text);
this._onDidInputData.fire(text);
this._onDidSendText.fire(text);
this.xterm?.scrollToBottom();
if (shouldExecute) {
this._onDidExecuteText.fire();
}
}
async sendPath(originalPath: string | URI, shouldExecute: boolean): Promise<void> {
return this.sendText(await this.preparePathForShell(originalPath), shouldExecute);
}
async preparePathForShell(originalPath: string | URI): Promise<string> {
// Wait for shell type to be ready
await this.processReady;
return preparePathForShell(originalPath, this.shellLaunchConfig.executable, this.title, this.shellType, this._processManager.backend, this._processManager.os);
}
setVisible(visible: boolean): void {
const didChange = this._isVisible !== visible;
this._isVisible = visible;
this._wrapperElement.classList.toggle('active', visible);
if (visible && this.xterm) {
this._open();
// Flush any pending resizes
this._resizeDebouncer?.flush();
// Resize to re-evaluate dimensions, this will ensure when switching to a terminal it is
// using the most up to date dimensions (eg. when terminal is created in the background
// using cached dimensions of a split terminal).
this._resize();
}
if (didChange) {
this._onDidChangeVisibility.fire(visible);
}
}
scrollDownLine(): void {
this.xterm?.scrollDownLine();
}
scrollDownPage(): void {
this.xterm?.scrollDownPage();
}
scrollToBottom(): void {
this.xterm?.scrollToBottom();
}
scrollUpLine(): void {
this.xterm?.scrollUpLine();
}
scrollUpPage(): void {
this.xterm?.scrollUpPage();
}
scrollToTop(): void {
this.xterm?.scrollToTop();
}
clearBuffer(): void {
this._processManager.clearBuffer();
this.xterm?.clearBuffer();
}
private _refreshSelectionContextKey() {
const isActive = !!this._viewsService.getActiveViewWithId(TERMINAL_VIEW_ID);
let isEditorActive = false;
const editor = this._editorService.activeEditor;
if (editor) {
isEditorActive = editor instanceof TerminalEditorInput;
}
this._terminalHasTextContextKey.set((isActive || isEditorActive) && this.hasSelection());
}
protected _createProcessManager(): TerminalProcessManager {
let deserializedCollections: ReadonlyMap<string, IEnvironmentVariableCollection> | undefined;
if (this.shellLaunchConfig.attachPersistentProcess?.environmentVariableCollections) {
deserializedCollections = deserializeEnvironmentVariableCollections(this.shellLaunchConfig.attachPersistentProcess.environmentVariableCollections);
}
const processManager = this._scopedInstantiationService.createInstance(
TerminalProcessManager,
this._instanceId,
this.shellLaunchConfig?.cwd,
deserializedCollections,
this.shellLaunchConfig.attachPersistentProcess?.shellIntegrationNonce
);
this.capabilities.add(processManager.capabilities);
this._register(processManager.onProcessReady(async (e) => {
this._onProcessIdReady.fire(this);
this._initialCwd = await this.getInitialCwd();
// Set the initial name based on the _resolved_ shell launch config, this will also
// ensure the resolved icon gets shown
if (!this._labelComputer) {
this._labelComputer = this._register(this._scopedInstantiationService.createInstance(TerminalLabelComputer));
this._register(this._labelComputer.onDidChangeLabel(e => {
const wasChanged = this._title !== e.title || this._description !== e.description;
if (wasChanged) {
this._title = e.title;
this._description = e.description;
this._onTitleChanged.fire(this);
}
}));
}
if (this._shellLaunchConfig.name) {
this._setTitle(this._shellLaunchConfig.name, TitleEventSource.Api);
} else {
// Listen to xterm.js' sequence title change event, trigger this async to ensure
// _xtermReadyPromise is ready constructed since this is called from the ctor
setTimeout(() => {
this._xtermReadyPromise.then(xterm => {
if (xterm) {
this._messageTitleDisposable.value = xterm.raw.onTitleChange(e => this._onTitleChange(e));
}
});
});
this._setTitle(this._shellLaunchConfig.executable, TitleEventSource.Process);
}
}));
this._register(processManager.onProcessExit(exitCode => this._onProcessExit(exitCode)));
this._register(processManager.onDidChangeProperty(({ type, value }) => {
switch (type) {
case ProcessPropertyType.Cwd:
this._cwd = value;
this._labelComputer?.refreshLabel(this);
break;
case ProcessPropertyType.InitialCwd:
this._initialCwd = value;
this._cwd = this._initialCwd;
this._setTitle(this.title, TitleEventSource.Config);
this._icon = this._shellLaunchConfig.attachPersistentProcess?.icon || this._shellLaunchConfig.icon;
this._onIconChanged.fire({ instance: this, userInitiated: false });
break;
case ProcessPropertyType.Title:
this._setTitle(value ?? '', TitleEventSource.Process);
break;
case ProcessPropertyType.OverrideDimensions:
this.setOverrideDimensions(value, true);
break;
case ProcessPropertyType.ResolvedShellLaunchConfig:
this._setResolvedShellLaunchConfig(value);
break;
case ProcessPropertyType.ShellType:
this.setShellType(value);
break;
case ProcessPropertyType.HasChildProcesses:
this._onDidChangeHasChildProcesses.fire(value);
break;
case ProcessPropertyType.UsedShellIntegrationInjection:
this._usedShellIntegrationInjection = true;
break;
}
}));
this._initialDataEventsListener.value = processManager.onProcessData(ev => this._initialDataEvents?.push(ev.data));
this._register(processManager.onProcessReplayComplete(() => this._onProcessReplayComplete.fire()));
this._register(processManager.onEnvironmentVariableInfoChanged(e => this._onEnvironmentVariableInfoChanged(e)));
this._register(processManager.onPtyDisconnect(() => {
if (this.xterm) {
this.xterm.raw.options.disableStdin = true;
}
this.statusList.add({
id: TerminalStatus.Disconnected,
severity: Severity.Error,
icon: Codicon.debugDisconnect,
tooltip: nls.localize('disconnectStatus', "Lost connection to process")
});
}));
this._register(processManager.onPtyReconnect(() => {
if (this.xterm) {
this.xterm.raw.options.disableStdin = false;
}
this.statusList.remove(TerminalStatus.Disconnected);
}));
return processManager;
}
private async _createProcess(): Promise<void> {
if (this.isDisposed) {
return;
}
const activeWorkspaceRootUri = this._historyService.getLastActiveWorkspaceRoot(Schemas.file);
if (activeWorkspaceRootUri) {
const trusted = await this._trust();
if (!trusted) {
this._onProcessExit({ message: nls.localize('workspaceNotTrustedCreateTerminal', "Cannot launch a terminal process in an untrusted workspace") });
}
} else if (this._cwd && this._userHome && this._cwd !== this._userHome) {
// something strange is going on if cwd is not userHome in an empty workspace
this._onProcessExit({
message: nls.localize('workspaceNotTrustedCreateTerminalCwd', "Cannot launch a terminal process in an untrusted workspace with cwd {0} and userHome {1}", this._cwd, this._userHome)
});
}
// Re-evaluate dimensions if the container has been set since the xterm instance was created
if (this._container && this._cols === 0 && this._rows === 0) {
this._initDimensions();
this.xterm?.raw.resize(this._cols || Constants.DefaultCols, this._rows || Constants.DefaultRows);
}
const originalIcon = this.shellLaunchConfig.icon;
await this._processManager.createProcess(this._shellLaunchConfig, this._cols || Constants.DefaultCols, this._rows || Constants.DefaultRows).then(result => {
if (result) {
if ('message' in result) {
this._onProcessExit(result);
} else if ('injectedArgs' in result) {
this._injectedArgs = result.injectedArgs;
}
}
});
if (this.isDisposed) {
return;
}
if (this.xterm?.shellIntegration) {
this.capabilities.add(this.xterm.shellIntegration.capabilities);
}
if (originalIcon !== this.shellLaunchConfig.icon || this.shellLaunchConfig.color) {
this._icon = this._shellLaunchConfig.attachPersistentProcess?.icon || this._shellLaunchConfig.icon;
this._onIconChanged.fire({ instance: this, userInitiated: false });
}
}
public registerMarker(offset?: number): IMarker | undefined {
return this.xterm?.raw.registerMarker(offset);
}
public addBufferMarker(properties: IMarkProperties): void {
this.capabilities.get(TerminalCapability.BufferMarkDetection)?.addMark(properties);
}
public scrollToMark(startMarkId: string, endMarkId?: string, highlight?: boolean): void {
this.xterm?.markTracker.scrollToClosestMarker(startMarkId, endMarkId, highlight);
}
public async freePortKillProcess(port: string, command: string): Promise<void> {
await this._processManager?.freePortKillProcess(port);
this.runCommand(command, false);
}
private _onProcessData(ev: IProcessDataEvent): void {
// Ensure events are split by SI command execute sequence to ensure the output of the
// command can be read by extensions. This must be done here as xterm.js does not currently
// have a listener for when individual data events are parsed, only `onWriteParsed` which
// fires when the write buffer is flushed.
const execIndex = ev.data.indexOf('\x1b]633;C\x07');
if (execIndex !== -1) {
if (ev.trackCommit) {
this._writeProcessData(ev.data.substring(0, execIndex + '\x1b]633;C\x07'.length));
ev.writePromise = new Promise<void>(r => this._writeProcessData(ev.data.substring(execIndex + '\x1b]633;C\x07'.length), r));
} else {
this._writeProcessData(ev.data.substring(0, execIndex + '\x1b]633;C\x07'.length));
this._writeProcessData(ev.data.substring(execIndex + '\x1b]633;C\x07'.length));
}
} else {
if (ev.trackCommit) {
ev.writePromise = new Promise<void>(r => this._writeProcessData(ev.data, r));
} else {
this._writeProcessData(ev.data);
}
}
}
private _writeProcessData(data: string, cb?: () => void) {
this._onWillData.fire(data);
const messageId = ++this._latestXtermWriteData;
this.xterm?.raw.write(data, () => {
this._latestXtermParseData = messageId;
this._processManager.acknowledgeDataEvent(data.length);
cb?.();
this._onData.fire(data);
});
}
/**
* Called when either a process tied to a terminal has exited or when a terminal renderer
* simulates a process exiting (e.g. custom execution task).
* @param exitCode The exit code of the process, this is undefined when the terminal was exited
* through user action.
*/
private async _onProcessExit(exitCodeOrError?: number | ITerminalLaunchError): Promise<void> {
// Prevent dispose functions being triggered multiple times
if (this._isExiting) {
return;
}
const parsedExitResult = parseExitResult(exitCodeOrError, this.shellLaunchConfig, this._processManager.processState, this._initialCwd);
if (this._usedShellIntegrationInjection && this._processManager.processState === ProcessState.KilledDuringLaunch && parsedExitResult?.code !== 0) {
this._relaunchWithShellIntegrationDisabled(parsedExitResult?.message);
this._onExit.fire(exitCodeOrError);
return;
}
this._isExiting = true;
await this._flushXtermData();
this._exitCode = parsedExitResult?.code;
const exitMessage = parsedExitResult?.message;
this._logService.debug('Terminal process exit', 'instanceId', this.instanceId, 'code', this._exitCode, 'processState', this._processManager.processState);
// Only trigger wait on exit when the exit was *not* triggered by the
// user (via the `workbench.action.terminal.kill` command).
const waitOnExit = this.waitOnExit;
if (waitOnExit && this._processManager.processState !== ProcessState.KilledByUser) {
this._xtermReadyPromise.then(xterm => {
if (!xterm) {
return;
}
if (exitMessage) {
xterm.raw.write(formatMessageForTerminal(exitMessage));
}
switch (typeof waitOnExit) {
case 'string':
xterm.raw.write(formatMessageForTerminal(waitOnExit, { excludeLeadingNewLine: true }));
break;
case 'function':
if (this.exitCode !== undefined) {
xterm.raw.write(formatMessageForTerminal(waitOnExit(this.exitCode), { excludeLeadingNewLine: true }));
}
break;
}
// Disable all input if the terminal is exiting and listen for next keypress
xterm.raw.options.disableStdin = true;
if (xterm.raw.textarea) {
this._attachPressAnyKeyToCloseListener(xterm.raw);
}
});
} else {
if (exitMessage) {
const failedDuringLaunch = this._processManager.processState === ProcessState.KilledDuringLaunch;
if (failedDuringLaunch || (this._terminalConfigurationService.config.showExitAlert && this.xterm?.lastInputEvent !== /*Ctrl+D*/'\x04')) {
// Always show launch failures
this._notificationService.notify({
message: exitMessage,
severity: Severity.Error,
actions: { primary: [this._scopedInstantiationService.createInstance(TerminalLaunchHelpAction)] }
});
} else {
// Log to help surface the error in case users report issues with showExitAlert
// disabled
this._logService.warn(exitMessage);
}
}
this.dispose(TerminalExitReason.Process);
}
// First onExit to consumers, this can happen after the terminal has already been disposed.
this._onExit.fire(exitCodeOrError);
// Dispose of the onExit event if the terminal will not be reused again
if (this.isDisposed) {
this._onExit.dispose();
}
}
private _relaunchWithShellIntegrationDisabled(exitMessage: string | undefined): void {
this._shellLaunchConfig.ignoreShellIntegration = true;
this.relaunch();
this.statusList.add({
id: TerminalStatus.ShellIntegrationAttentionNeeded,
severity: Severity.Warning,
icon: Codicon.warning,
tooltip: `${exitMessage} ` + nls.localize('launchFailed.exitCodeOnlyShellIntegration', 'Disabling shell integration in user settings might help.'),
hoverActions: [{
commandId: TerminalCommandId.ShellIntegrationLearnMore,
label: nls.localize('shellIntegration.learnMore', "Learn more about shell integration"),
run: () => {
this._openerService.open('https://code.visualstudio.com/docs/editor/integrated-terminal#_shell-integration');
}
}, {
commandId: 'workbench.action.openSettings',
label: nls.localize('shellIntegration.openSettings', "Open user settings"),
run: () => {
this._commandService.executeCommand('workbench.action.openSettings', 'terminal.integrated.shellIntegration.enabled');
}
}]
});
this._telemetryService.publicLog2<{}, { owner: 'meganrogge'; comment: 'Indicates the process exited when created with shell integration args' }>('terminal/shellIntegrationFailureProcessExit');
}
/**
* Ensure write calls to xterm.js have finished before resolving.
*/
private _flushXtermData(): Promise<void> {
if (this._latestXtermWriteData === this._latestXtermParseData) {
return Promise.resolve();
}
let retries = 0;
return new Promise<void>(r => {
const interval = dom.disposableWindowInterval(dom.getActiveWindow().window, () => {
if (this._latestXtermWriteData === this._latestXtermParseData || ++retries === 5) {
interval.dispose();
r();
}
}, 20);
});
}
private _attachPressAnyKeyToCloseListener(xterm: XTermTerminal) {
if (xterm.textarea && !this._pressAnyKeyToCloseListener) {
this._pressAnyKeyToCloseListener = dom.addDisposableListener(xterm.textarea, 'keypress', (event: KeyboardEvent) => {
if (this._pressAnyKeyToCloseListener) {
this._pressAnyKeyToCloseListener.dispose();
this._pressAnyKeyToCloseListener = undefined;
this.dispose(TerminalExitReason.Process);
event.preventDefault();
}
});
}
}
private _writeInitialText(xterm: XtermTerminal, callback?: () => void): void {
if (!this._shellLaunchConfig.initialText) {
callback?.();
return;
}
const text = typeof this._shellLaunchConfig.initialText === 'string'
? this._shellLaunchConfig.initialText
: this._shellLaunchConfig.initialText?.text;
if (typeof this._shellLaunchConfig.initialText === 'string') {
xterm.raw.writeln(text, callback);
} else {
if (this._shellLaunchConfig.initialText.trailingNewLine) {
xterm.raw.writeln(text, callback);
} else {
xterm.raw.write(text, callback);
}
}
}
async reuseTerminal(shell: IShellLaunchConfig, reset: boolean = false): Promise<void> {
// Unsubscribe any key listener we may have.
this._pressAnyKeyToCloseListener?.dispose();
this._pressAnyKeyToCloseListener = undefined;
const xterm = this.xterm;
if (xterm) {
if (!reset) {
// Ensure new processes' output starts at start of new line
await new Promise<void>(r => xterm.raw.write('\n\x1b[G', r));
}
// Print initialText if specified
if (shell.initialText) {
this._shellLaunchConfig.initialText = shell.initialText;
await new Promise<void>(r => this._writeInitialText(xterm, r));
}
// Clean up waitOnExit state
if (this._isExiting && this._shellLaunchConfig.waitOnExit) {
xterm.raw.options.disableStdin = false;
this._isExiting = false;
}
if (reset) {
xterm.clearDecorations();
}
}
// Dispose the environment info widget if it exists
this.statusList.remove(TerminalStatus.RelaunchNeeded);
if (!reset) {
// HACK: Force initialText to be non-falsy for reused terminals such that the
// conptyInheritCursor flag is passed to the node-pty, this flag can cause a Window to stop
// responding in Windows 10 1903 so we only want to use it when something is definitely written
// to the terminal.
shell.initialText = ' ';
}
// Set the new shell launch config
this._shellLaunchConfig = shell; // Must be done before calling _createProcess()
await this._processManager.relaunch(this._shellLaunchConfig, this._cols || Constants.DefaultCols, this._rows || Constants.DefaultRows, reset).then(result => {
if (result) {
if ('message' in result) {
this._onProcessExit(result);
} else if ('injectedArgs' in result) {
this._injectedArgs = result.injectedArgs;
}
}
});
}
@debounce(1000)
relaunch(): void {
this.reuseTerminal(this._shellLaunchConfig, true);
}
private _onTitleChange(title: string): void {
if (this.isTitleSetByProcess) {
this._setTitle(title, TitleEventSource.Sequence);
}
}
private async _trust(): Promise<boolean> {
return (await this._workspaceTrustRequestService.requestWorkspaceTrust(
{
message: nls.localize('terminal.requestTrust', "Creating a terminal process requires executing code")
})) === true;
}
@debounce(2000)
private async _updateProcessCwd(): Promise<void> {
if (this.isDisposed || this.shellLaunchConfig.customPtyImplementation) {
return;
}
// reset cwd if it has changed, so file based url paths can be resolved
try {
const cwd = await this._refreshProperty(ProcessPropertyType.Cwd);
if (typeof cwd !== 'string') {
throw new Error(`cwd is not a string ${cwd}`);
}
} catch (e: unknown) {
// Swallow this as it means the process has been killed
if (e instanceof Error && e.message === 'Cannot refresh property when process is not set') {
return;
}
throw e;
}
}
updateConfig(): void {
this._setCommandsToSkipShell(this._terminalConfigurationService.config.commandsToSkipShell);
this._refreshEnvironmentVariableInfoWidgetState(this._processManager.environmentVariableInfo);
}
private async _updateUnicodeVersion(): Promise<void> {
this._processManager.setUnicodeVersion(this._terminalConfigurationService.config.unicodeVersion);
}
updateAccessibilitySupport(): void {
this.xterm!.raw.options.screenReaderMode = this._accessibilityService.isScreenReaderOptimized();
}
private _setCommandsToSkipShell(commands: string[]): void {
const excludeCommands = commands.filter(command => command[0] === '-').map(command => command.slice(1));
this._skipTerminalCommands = DEFAULT_COMMANDS_TO_SKIP_SHELL.filter(defaultCommand => {
return !excludeCommands.includes(defaultCommand);
}).concat(commands);
}
layout(dimension: dom.Dimension): void {
this._lastLayoutDimensions = dimension;
if (this.disableLayout) {
return;
}
// Don't layout if dimensions are invalid (eg. the container is not attached to the DOM or
// if display: none
if (dimension.width <= 0 || dimension.height <= 0) {
return;
}
// Evaluate columns and rows, exclude the wrapper element's margin
const terminalWidth = this._evaluateColsAndRows(dimension.width, dimension.height);
if (!terminalWidth) {
return;
}
this._resize();
// Signal the container is ready
if (!this._containerReadyBarrier.isOpen()) {
this._containerReadyBarrier.open();
}
// Layout all contributions
for (const contribution of this._contributions.values()) {
if (!this.xterm) {
this._xtermReadyPromise.then(xterm => {
if (xterm) {
contribution.layout?.(xterm, dimension);
}
});
} else {
contribution.layout?.(this.xterm, dimension);
}
}
}
private async _resize(immediate?: boolean): Promise<void> {
if (!this.xterm) {
return;
}
let cols = this.cols;
let rows = this.rows;
// Only apply these settings when the terminal is visible so that
// the characters are measured correctly.
if (this._isVisible && this._layoutSettingsChanged) {
const font = this.xterm.getFont();
const config = this._terminalConfigurationService.config;
this.xterm.raw.options.letterSpacing = font.letterSpacing;
this.xterm.raw.options.lineHeight = font.lineHeight;
this.xterm.raw.options.fontSize = font.fontSize;
this.xterm.raw.options.fontFamily = font.fontFamily;
this.xterm.raw.options.fontWeight = config.fontWeight;
this.xterm.raw.options.fontWeightBold = config.fontWeightBold;
// Any of the above setting changes could have changed the dimensions of the
// terminal, re-evaluate now.
this._initDimensions();
cols = this.cols;
rows = this.rows;
this._layoutSettingsChanged = false;
}
if (isNaN(cols) || isNaN(rows)) {
return;
}
if (cols !== this.xterm.raw.cols || rows !== this.xterm.raw.rows) {
if (this._fixedRows || this._fixedCols) {
await this._updateProperty(ProcessPropertyType.FixedDimensions, { cols: this._fixedCols, rows: this._fixedRows });
}
this._onDimensionsChanged.fire();
}
TerminalInstance._lastKnownGridDimensions = { cols, rows };
this._resizeDebouncer!.resize(cols, rows, immediate ?? false);
}
private async _updatePtyDimensions(rawXterm: XTermTerminal): Promise<void> {
await this._processManager.setDimensions(rawXterm.cols, rawXterm.rows);
}
setShellType(shellType: TerminalShellType | undefined) {
if (this._shellType === shellType) {
return;
}
if (shellType) {
this._shellType = shellType;
this._terminalShellTypeContextKey.set(shellType?.toString());
this._onDidChangeShellType.fire(shellType);
}
}
private _setAriaLabel(xterm: XTermTerminal | undefined, terminalId: number, title: string | undefined): void {
const labelParts: string[] = [];
if (xterm && xterm.textarea) {
if (title && title.length > 0) {
labelParts.push(nls.localize('terminalTextBoxAriaLabelNumberAndTitle', "Terminal {0}, {1}", terminalId, title));
} else {
labelParts.push(nls.localize('terminalTextBoxAriaLabel', "Terminal {0}", terminalId));
}
const screenReaderOptimized = this._accessibilityService.isScreenReaderOptimized();
if (!screenReaderOptimized) {
labelParts.push(nls.localize('terminalScreenReaderMode', "Run the command: Toggle Screen Reader Accessibility Mode for an optimized screen reader experience"));
}
const accessibilityHelpKeybinding = this._keybindingService.lookupKeybinding(AccessibilityCommandId.OpenAccessibilityHelp)?.getLabel();
if (this._configurationService.getValue(AccessibilityVerbositySettingId.Terminal) && accessibilityHelpKeybinding) {
labelParts.push(nls.localize('terminalHelpAriaLabel', "Use {0} for terminal accessibility help", accessibilityHelpKeybinding));
}
xterm.textarea.setAttribute('aria-label', labelParts.join('\n'));
}
}
private _updateTitleProperties(title: string | undefined, eventSource: TitleEventSource): string {
if (!title) {
return this._processName;
}
switch (eventSource) {
case TitleEventSource.Process:
if (this._processManager.os === OperatingSystem.Windows) {
// Extract the file name without extension
title = path.win32.parse(title).name;
} else {
const firstSpaceIndex = title.indexOf(' ');
if (title.startsWith('/')) {
title = path.basename(title);
} else if (firstSpaceIndex > -1) {
title = title.substring(0, firstSpaceIndex);
}
}
this._processName = title;
break;
case TitleEventSource.Api:
// If the title has not been set by the API or the rename command, unregister the handler that
// automatically updates the terminal name
this._staticTitle = title;
this._messageTitleDisposable.value = undefined;
break;
case TitleEventSource.Sequence:
// On Windows, some shells will fire this with the full path which we want to trim
// to show just the file name. This should only happen if the title looks like an
// absolute Windows file path
this._sequence = title;
if (this._processManager.os === OperatingSystem.Windows &&
title.match(/^[a-zA-Z]:\\.+\.[a-zA-Z]{1,3}/)) {
this._sequence = path.win32.parse(title).name;
}
break;
}
this._titleSource = eventSource;
return title;
}
setOverrideDimensions(dimensions: ITerminalDimensionsOverride | undefined, immediate: boolean = false): void {
if (this._dimensionsOverride && this._dimensionsOverride.forceExactSize && !dimensions && this._rows === 0 && this._cols === 0) {
// this terminal never had a real size => keep the last dimensions override exact size
this._cols = this._dimensionsOverride.cols;
this._rows = this._dimensionsOverride.rows;
}
this._dimensionsOverride = dimensions;
if (immediate) {
this._resize(true);
} else {
this._resize();
}
}
async setFixedDimensions(): Promise<void> {
const cols = await this._quickInputService.input({
title: nls.localize('setTerminalDimensionsColumn', "Set Fixed Dimensions: Column"),
placeHolder: 'Enter a number of columns or leave empty for automatic width',
validateInput: async (text) => text.length > 0 && !text.match(/^\d+$/) ? { content: 'Enter a number or leave empty size automatically', severity: Severity.Error } : undefined
});
if (cols === undefined) {
return;
}
this._fixedCols = this._parseFixedDimension(cols);
this._labelComputer?.refreshLabel(this);
this._terminalHasFixedWidth.set(!!this._fixedCols);
const rows = await this._quickInputService.input({
title: nls.localize('setTerminalDimensionsRow', "Set Fixed Dimensions: Row"),
placeHolder: 'Enter a number of rows or leave empty for automatic height',
validateInput: async (text) => text.length > 0 && !text.match(/^\d+$/) ? { content: 'Enter a number or leave empty size automatically', severity: Severity.Error } : undefined
});
if (rows === undefined) {
return;
}
this._fixedRows = this._parseFixedDimension(rows);
this._labelComputer?.refreshLabel(this);
await this._refreshScrollbar();
this._resize();
this.focus();
}
private _parseFixedDimension(value: string): number | undefined {
if (value === '') {
return undefined;
}
const parsed = parseInt(value);
if (parsed <= 0) {
throw new Error(`Could not parse dimension "${value}"`);
}
return parsed;
}
async toggleSizeToContentWidth(): Promise<void> {
if (!this.xterm?.raw.buffer.active) {
return;
}
if (this._hasScrollBar) {
this._terminalHasFixedWidth.set(false);
this._fixedCols = undefined;
this._fixedRows = undefined;
this._hasScrollBar = false;
this._initDimensions();
await this._resize();
} else {
const font = this.xterm ? this.xterm.getFont() : this._terminalConfigurationService.getFont(dom.getWindow(this.domElement));
const maxColsForTexture = Math.floor(Constants.MaxCanvasWidth / (font.charWidth ?? 20));
// Fixed columns should be at least xterm.js' regular column count
const proposedCols = Math.max(this.maxCols, Math.min(this.xterm.getLongestViewportWrappedLineLength(), maxColsForTexture));
// Don't switch to fixed dimensions if the content already fits as it makes the scroll
// bar look bad being off the edge
if (proposedCols > this.xterm.raw.cols) {
this._fixedCols = proposedCols;
}
}
await this._refreshScrollbar();
this._labelComputer?.refreshLabel(this);
this.focus();
}
private _refreshScrollbar(): Promise<void> {
if (this._fixedCols || this._fixedRows) {
return this._addScrollbar();
}
return this._removeScrollbar();
}
private async _addScrollbar(): Promise<void> {
const charWidth = (this.xterm ? this.xterm.getFont() : this._terminalConfigurationService.getFont(dom.getWindow(this.domElement))).charWidth;
if (!this.xterm?.raw.element || !this._container || !charWidth || !this._fixedCols) {
return;
}
this._wrapperElement.classList.add('fixed-dims');
this._hasScrollBar = true;
this._initDimensions();
await this._resize();
this._terminalHasFixedWidth.set(true);
if (!this._horizontalScrollbar) {
this._horizontalScrollbar = this._register(new DomScrollableElement(this._wrapperElement, {
vertical: ScrollbarVisibility.Hidden,
horizontal: ScrollbarVisibility.Auto,
useShadows: false,
scrollYToX: false,
consumeMouseWheelIfScrollbarIsNeeded: false
}));
this._container.appendChild(this._horizontalScrollbar.getDomNode());
}
this._horizontalScrollbar.setScrollDimensions({
width: this.xterm.raw.element.clientWidth,
scrollWidth: this._fixedCols * charWidth + 40 // Padding + scroll bar
});
this._horizontalScrollbar.getDomNode().style.paddingBottom = '16px';
// work around for https://github.com/xtermjs/xterm.js/issues/3482
if (isWindows) {
for (let i = this.xterm.raw.buffer.active.viewportY; i < this.xterm.raw.buffer.active.length; i++) {
const line = this.xterm.raw.buffer.active.getLine(i);
(line as any)._line.isWrapped = false;
}
}
}
private async _removeScrollbar(): Promise<void> {
if (!this._container || !this._horizontalScrollbar) {
return;
}
this._horizontalScrollbar.getDomNode().remove();
this._horizontalScrollbar.dispose();
this._horizontalScrollbar = undefined;
this._wrapperElement.remove();
this._wrapperElement.classList.remove('fixed-dims');
this._container.appendChild(this._wrapperElement);
}
private _setResolvedShellLaunchConfig(shellLaunchConfig: IShellLaunchConfig): void {
this._shellLaunchConfig.args = shellLaunchConfig.args;
this._shellLaunchConfig.cwd = shellLaunchConfig.cwd;
this._shellLaunchConfig.executable = shellLaunchConfig.executable;
this._shellLaunchConfig.env = shellLaunchConfig.env;
}
private _onEnvironmentVariableInfoChanged(info: IEnvironmentVariableInfo): void {
if (info.requiresAction) {
this.xterm?.raw.textarea?.setAttribute('aria-label', nls.localize('terminalStaleTextBoxAriaLabel', "Terminal {0} environment is stale, run the 'Show Environment Information' command for more information", this._instanceId));
}
this._refreshEnvironmentVariableInfoWidgetState(info);
}
private async _refreshEnvironmentVariableInfoWidgetState(info?: IEnvironmentVariableInfo): Promise<void> {
// Check if the status should exist
if (!info) {
this.statusList.remove(TerminalStatus.RelaunchNeeded);
this.statusList.remove(TerminalStatus.EnvironmentVariableInfoChangesActive);
return;
}
// Recreate the process seamlessly without informing the use if the following conditions are
// met.
if (
// The change requires a relaunch
info.requiresAction &&
// The feature is enabled
this._terminalConfigurationService.config.environmentChangesRelaunch &&
// Has not been interacted with
!this._processManager.hasWrittenData &&
// Not a feature terminal or is a reconnecting task terminal (TODO: Need to explain the latter case)
(!this._shellLaunchConfig.isFeatureTerminal || (this.reconnectionProperties && this._configurationService.getValue('task.reconnection') === true)) &&
// Not a custom pty
!this._shellLaunchConfig.customPtyImplementation &&
// Not an extension owned terminal
!this._shellLaunchConfig.isExtensionOwnedTerminal &&
// Not a reconnected or revived terminal
!this._shellLaunchConfig.attachPersistentProcess &&
// Not a Windows remote using ConPTY (#187084)
!(this._processManager.remoteAuthority && this._terminalConfigurationService.config.windowsEnableConpty && (await this._processManager.getBackendOS()) === OperatingSystem.Windows)
) {
this.relaunch();
return;
}
// Re-create statuses
const workspaceFolder = getWorkspaceForTerminal(this.shellLaunchConfig.cwd, this._workspaceContextService, this._historyService);
this.statusList.add(info.getStatus({ workspaceFolder }));
}
async getInitialCwd(): Promise<string> {
if (!this._initialCwd) {
this._initialCwd = this._processManager.initialCwd;
}
return this._initialCwd;
}
async getCwd(): Promise<string> {
if (this.capabilities.has(TerminalCapability.CwdDetection)) {
return this.capabilities.get(TerminalCapability.CwdDetection)!.getCwd();
} else if (this.capabilities.has(TerminalCapability.NaiveCwdDetection)) {
return this.capabilities.get(TerminalCapability.NaiveCwdDetection)!.getCwd();
}
return this._processManager.initialCwd;
}
private async _refreshProperty<T extends ProcessPropertyType>(type: T): Promise<IProcessPropertyMap[T]> {
await this.processReady;
return this._processManager.refreshProperty(type);
}
private async _updateProperty<T extends ProcessPropertyType>(type: T, value: IProcessPropertyMap[T]): Promise<void> {
return this._processManager.updateProperty(type, value);
}
async rename(title?: string) {
this._setTitle(title, TitleEventSource.Api);
}
private _setTitle(title: string | undefined, eventSource: TitleEventSource): void {
const reset = !title;
title = this._updateTitleProperties(title, eventSource);
const titleChanged = title !== this._title;
this._title = title;
this._labelComputer?.refreshLabel(this, reset);
this._setAriaLabel(this.xterm?.raw, this._instanceId, this._title);
if (titleChanged) {
this._onTitleChanged.fire(this);
}
}
async changeIcon(icon?: TerminalIcon): Promise<TerminalIcon | undefined> {
if (icon) {
this._icon = icon;
this._onIconChanged.fire({ instance: this, userInitiated: true });
return icon;
}
const iconPicker = this._scopedInstantiationService.createInstance(TerminalIconPicker);
const pickedIcon = await iconPicker.pickIcons();
iconPicker.dispose();
if (!pickedIcon) {
return undefined;
}
this._icon = pickedIcon;
this._onIconChanged.fire({ instance: this, userInitiated: true });
return pickedIcon;
}
async changeColor(color?: string, skipQuickPick?: boolean): Promise<string | undefined> {
if (color) {
this.shellLaunchConfig.color = color;
this._onIconChanged.fire({ instance: this, userInitiated: true });
return color;
} else if (skipQuickPick) {
// Reset this tab's color
this.shellLaunchConfig.color = '';
this._onIconChanged.fire({ instance: this, userInitiated: true });
return;
}
const icon = this._getIcon();
if (!icon) {
return;
}
const colorTheme = this._themeService.getColorTheme();
const standardColors: string[] = getStandardColors(colorTheme);
const colorStyleDisposable = createColorStyleElement(colorTheme);
const items: QuickPickItem[] = [];
for (const colorKey of standardColors) {
const colorClass = getColorClass(colorKey);
items.push({
label: `$(${Codicon.circleFilled.id}) ${colorKey.replace('terminal.ansi', '')}`, id: colorKey, description: colorKey, iconClasses: [colorClass]
});
}
items.push({ type: 'separator' });
const showAllColorsItem = { label: 'Reset to default' };
items.push(showAllColorsItem);
const disposables: IDisposable[] = [];
const quickPick = this._quickInputService.createQuickPick({ useSeparators: true });
disposables.push(quickPick);
quickPick.items = items;
quickPick.matchOnDescription = true;
quickPick.placeholder = nls.localize('changeColor', 'Select a color for the terminal');
quickPick.show();
const result = await new Promise<IQuickPickItem | undefined>(r => {
disposables.push(quickPick.onDidHide(() => r(undefined)));
disposables.push(quickPick.onDidAccept(() => r(quickPick.selectedItems[0])));
});
dispose(disposables);
if (result) {
this.shellLaunchConfig.color = result.id;
this._onIconChanged.fire({ instance: this, userInitiated: true });
}
quickPick.hide();
colorStyleDisposable.dispose();
return result?.id;
}
forceScrollbarVisibility(): void {
this._wrapperElement.classList.add('force-scrollbar');
}
resetScrollbarVisibility(): void {
this._wrapperElement.classList.remove('force-scrollbar');
}
setParentContextKeyService(parentContextKeyService: IContextKeyService): void {
this._scopedContextKeyService.updateParent(parentContextKeyService);
}
async handleMouseEvent(event: MouseEvent, contextMenu: IMenu): Promise<{ cancelContextMenu: boolean } | void> {
// Don't handle mouse event if it was on the scroll bar
if (dom.isHTMLElement(event.target) && (event.target.classList.contains('scrollbar') || event.target.classList.contains('slider'))) {
return { cancelContextMenu: true };
}
// Allow contributions to handle the mouse event first
for (const contrib of this._contributions.values()) {
const result = await contrib.handleMouseEvent?.(event);
if (result?.handled) {
return { cancelContextMenu: true };
}
}
// Middle click
if (event.which === 2) {
switch (this._terminalConfigurationService.config.middleClickBehavior) {
case 'default':
default:
// Drop selection and focus terminal on Linux to enable middle button paste
// when click occurs on the selection itself.
this.focus();
break;
}
return;
}
// Right click
if (event.which === 3) {
// Shift click forces the context menu
if (event.shiftKey) {
openContextMenu(dom.getActiveWindow(), event, this, contextMenu, this._contextMenuService);
return;
}
const rightClickBehavior = this._terminalConfigurationService.config.rightClickBehavior;
if (rightClickBehavior === 'nothing') {
if (!event.shiftKey) {
return { cancelContextMenu: true };
}
return;
}
}
}
}
class TerminalInstanceDragAndDropController extends Disposable implements dom.IDragAndDropObserverCallbacks {
private _dropOverlay?: HTMLElement;
private readonly _onDropFile = this._register(new Emitter<string | URI>());
get onDropFile(): Event<string | URI> { return this._onDropFile.event; }
private readonly _onDropTerminal = this._register(new Emitter<IRequestAddInstanceToGroupEvent>());
get onDropTerminal(): Event<IRequestAddInstanceToGroupEvent> { return this._onDropTerminal.event; }
constructor(
private readonly _container: HTMLElement,
@IWorkbenchLayoutService private readonly _layoutService: IWorkbenchLayoutService,
@IViewDescriptorService private readonly _viewDescriptorService: IViewDescriptorService,
) {
super();
this._register(toDisposable(() => this._clearDropOverlay()));
}
private _clearDropOverlay() {
this._dropOverlay?.remove();
this._dropOverlay = undefined;
}
onDragEnter(e: DragEvent) {
if (!containsDragType(e, DataTransfers.FILES, DataTransfers.RESOURCES, TerminalDataTransfers.Terminals, CodeDataTransfers.FILES)) {
return;
}
if (!this._dropOverlay) {
this._dropOverlay = document.createElement('div');
this._dropOverlay.classList.add('terminal-drop-overlay');
}
// Dragging terminals
if (containsDragType(e, TerminalDataTransfers.Terminals)) {
const side = this._getDropSide(e);
this._dropOverlay.classList.toggle('drop-before', side === 'before');
this._dropOverlay.classList.toggle('drop-after', side === 'after');
}
if (!this._dropOverlay.parentElement) {
this._container.appendChild(this._dropOverlay);
}
}
onDragLeave(e: DragEvent) {
this._clearDropOverlay();
}
onDragEnd(e: DragEvent) {
this._clearDropOverlay();
}
onDragOver(e: DragEvent) {
if (!e.dataTransfer || !this._dropOverlay) {
return;
}
// Dragging terminals
if (containsDragType(e, TerminalDataTransfers.Terminals)) {
const side = this._getDropSide(e);
this._dropOverlay.classList.toggle('drop-before', side === 'before');
this._dropOverlay.classList.toggle('drop-after', side === 'after');
}
this._dropOverlay.style.opacity = '1';
}
async onDrop(e: DragEvent) {
this._clearDropOverlay();
if (!e.dataTransfer) {
return;
}
const terminalResources = getTerminalResourcesFromDragEvent(e);
if (terminalResources) {
for (const uri of terminalResources) {
const side = this._getDropSide(e);
this._onDropTerminal.fire({ uri, side });
}
return;
}
// Check if files were dragged from the tree explorer
let path: URI | undefined;
const rawResources = e.dataTransfer.getData(DataTransfers.RESOURCES);
if (rawResources) {
path = URI.parse(JSON.parse(rawResources)[0]);
}
const rawCodeFiles = e.dataTransfer.getData(CodeDataTransfers.FILES);
if (!path && rawCodeFiles) {
path = URI.file(JSON.parse(rawCodeFiles)[0]);
}
if (!path && e.dataTransfer.files.length > 0 && getPathForFile(e.dataTransfer.files[0])) {
// Check if the file was dragged from the filesystem
path = URI.file(getPathForFile(e.dataTransfer.files[0])!);
}
if (!path) {
return;
}
this._onDropFile.fire(path);
}
private _getDropSide(e: DragEvent): 'before' | 'after' {
const target = this._container;
if (!target) {
return 'after';
}
const rect = target.getBoundingClientRect();
return this._getViewOrientation() === Orientation.HORIZONTAL
? (e.clientX - rect.left < rect.width / 2 ? 'before' : 'after')
: (e.clientY - rect.top < rect.height / 2 ? 'before' : 'after');
}
private _getViewOrientation(): Orientation {
const panelPosition = this._layoutService.getPanelPosition();
const terminalLocation = this._viewDescriptorService.getViewLocationById(TERMINAL_VIEW_ID);
return terminalLocation === ViewContainerLocation.Panel && isHorizontal(panelPosition)
? Orientation.HORIZONTAL
: Orientation.VERTICAL;
}
}
interface ITerminalLabelTemplateProperties {
cwd?: string | null | undefined;
cwdFolder?: string | null | undefined;
workspaceFolderName?: string | null | undefined;
workspaceFolder?: string | null | undefined;
local?: string | null | undefined;
process?: string | null | undefined;
sequence?: string | null | undefined;
progress?: string | null | undefined;
task?: string | null | undefined;
fixedDimensions?: string | null | undefined;
separator?: string | ISeparator | null | undefined;
shellType?: string | undefined;
shellCommand?: string | undefined;
shellPromptInput?: string | undefined;
}
const enum TerminalLabelType {
Title = 'title',
Description = 'description'
}
export class TerminalLabelComputer extends Disposable {
private _title: string = '';
private _description: string = '';
get title(): string | undefined { return this._title; }
get description(): string { return this._description; }
private readonly _onDidChangeLabel = this._register(new Emitter<{ title: string; description: string }>());
readonly onDidChangeLabel = this._onDidChangeLabel.event;
constructor(
@IFileService private readonly _fileService: IFileService,
@ITerminalConfigurationService private readonly _terminalConfigurationService: ITerminalConfigurationService,
@IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService
) {
super();
}
refreshLabel(instance: Pick<ITerminalInstance, 'shellLaunchConfig' | 'shellType' | 'cwd' | 'fixedCols' | 'fixedRows' | 'initialCwd' | 'processName' | 'sequence' | 'userHome' | 'workspaceFolder' | 'staticTitle' | 'capabilities' | 'title' | 'description'>, reset?: boolean): void {
this._title = this.computeLabel(instance, this._terminalConfigurationService.config.tabs.title, TerminalLabelType.Title, reset);
this._description = this.computeLabel(instance, this._terminalConfigurationService.config.tabs.description, TerminalLabelType.Description);
if (this._title !== instance.title || this._description !== instance.description || reset) {
this._onDidChangeLabel.fire({ title: this._title, description: this._description });
}
}
computeLabel(
instance: Pick<ITerminalInstance, 'shellLaunchConfig' | 'shellType' | 'cwd' | 'fixedCols' | 'fixedRows' | 'initialCwd' | 'processName' | 'sequence' | 'userHome' | 'workspaceFolder' | 'staticTitle' | 'capabilities' | 'title' | 'description' | 'progressState'>,
labelTemplate: string,
labelType: TerminalLabelType,
reset?: boolean
) {
const type = instance.shellLaunchConfig.attachPersistentProcess?.type || instance.shellLaunchConfig.type;
const commandDetection = instance.capabilities.get(TerminalCapability.CommandDetection);
const promptInputModel = commandDetection?.promptInputModel;
const nonTaskSpinner = type === 'Task' ? '' : ' $(loading~spin)';
const templateProperties: ITerminalLabelTemplateProperties = {
cwd: instance.cwd || instance.initialCwd || '',
cwdFolder: '',
workspaceFolderName: instance.workspaceFolder?.name,
workspaceFolder: instance.workspaceFolder ? path.basename(instance.workspaceFolder.uri.fsPath) : undefined,
local: type === 'Local' ? terminalStrings.typeLocal : undefined,
process: instance.processName,
sequence: instance.sequence,
task: type === 'Task' ? terminalStrings.typeTask : undefined,
fixedDimensions: instance.fixedCols
? (instance.fixedRows ? `\u2194${instance.fixedCols} \u2195${instance.fixedRows}` : `\u2194${instance.fixedCols}`)
: (instance.fixedRows ? `\u2195${instance.fixedRows}` : ''),
separator: { label: this._terminalConfigurationService.config.tabs.separator },
shellType: instance.shellType,
// Shell command requires high confidence
shellCommand: commandDetection?.executingCommand && commandDetection.executingCommandConfidence === 'high' && promptInputModel
? promptInputModel.value + nonTaskSpinner
: undefined,
// Shell prompt input does not require high confidence as it's largely for VS Code developers
shellPromptInput: commandDetection?.executingCommand && promptInputModel
? promptInputModel.getCombinedString(true) + nonTaskSpinner
: promptInputModel?.getCombinedString(true),
progress: this._getProgressStateString(instance.progressState)
};
templateProperties.workspaceFolderName = instance.workspaceFolder?.name ?? templateProperties.workspaceFolder;
labelTemplate = labelTemplate.trim();
if (!labelTemplate) {
return labelType === TerminalLabelType.Title ? (instance.processName || '') : '';
}
if (!reset && instance.staticTitle && labelType === TerminalLabelType.Title) {
return instance.staticTitle.replace(/[\n\r\t]/g, '') || templateProperties.process?.replace(/[\n\r\t]/g, '') || '';
}
const detection = instance.capabilities.has(TerminalCapability.CwdDetection) || instance.capabilities.has(TerminalCapability.NaiveCwdDetection);
const folders = this._workspaceContextService.getWorkspace().folders;
const multiRootWorkspace = folders.length > 1;
// Only set cwdFolder if detection is on
if (templateProperties.cwd && detection && (!instance.shellLaunchConfig.isFeatureTerminal || labelType === TerminalLabelType.Title)) {
const cwdUri = URI.from({
scheme: instance.workspaceFolder?.uri.scheme || Schemas.file,
path: instance.cwd ? path.resolve(instance.cwd) : undefined
});
// Multi-root workspaces always show cwdFolder to disambiguate them, otherwise only show
// when it differs from the workspace folder in which it was launched from
let showCwd = false;
if (multiRootWorkspace) {
showCwd = true;
} else if (instance.workspaceFolder?.uri) {
const caseSensitive = this._fileService.hasCapability(instance.workspaceFolder.uri, FileSystemProviderCapabilities.PathCaseSensitive);
showCwd = cwdUri.fsPath.localeCompare(instance.workspaceFolder.uri.fsPath, undefined, { sensitivity: caseSensitive ? 'case' : 'base' }) !== 0;
}
if (showCwd) {
templateProperties.cwdFolder = path.basename(templateProperties.cwd);
}
}
// Remove special characters that could mess with rendering
const label = template(labelTemplate, (templateProperties as unknown) as { [key: string]: string | ISeparator | undefined | null }).replace(/[\n\r\t]/g, '').trim();
return label === '' && labelType === TerminalLabelType.Title ? (instance.processName || '') : label;
}
private _getProgressStateString(progressState?: IProgressState): string {
if (!progressState) {
return '';
}
switch (progressState.state) {
case 0: return '';
case 1: return `${Math.round(progressState.value)}%`;
case 2: return '$(error)';
case 3: return '$(loading~spin)';
case 4: return '$(alert)';
}
}
}
export function parseExitResult(
exitCodeOrError: ITerminalLaunchError | number | undefined,
shellLaunchConfig: IShellLaunchConfig,
processState: ProcessState,
initialCwd: string | undefined
): { code: number | undefined; message: string | undefined } | undefined {
// Only return a message if the exit code is non-zero
if (exitCodeOrError === undefined || exitCodeOrError === 0) {
return { code: exitCodeOrError, message: undefined };
}
const code = typeof exitCodeOrError === 'number' ? exitCodeOrError : exitCodeOrError.code;
// Create exit code message
let message: string | undefined = undefined;
switch (typeof exitCodeOrError) {
case 'number': {
let commandLine: string | undefined = undefined;
if (shellLaunchConfig.executable) {
commandLine = shellLaunchConfig.executable;
if (typeof shellLaunchConfig.args === 'string') {
commandLine += ` ${shellLaunchConfig.args}`;
} else if (shellLaunchConfig.args && shellLaunchConfig.args.length) {
commandLine += shellLaunchConfig.args.map(a => ` '${a}'`).join();
}
}
if (processState === ProcessState.KilledDuringLaunch) {
if (commandLine) {
message = nls.localize('launchFailed.exitCodeAndCommandLine', "The terminal process \"{0}\" failed to launch (exit code: {1}).", commandLine, code);
} else {
message = nls.localize('launchFailed.exitCodeOnly', "The terminal process failed to launch (exit code: {0}).", code);
}
} else {
if (commandLine) {
message = nls.localize('terminated.exitCodeAndCommandLine', "The terminal process \"{0}\" terminated with exit code: {1}.", commandLine, code);
} else {
message = nls.localize('terminated.exitCodeOnly', "The terminal process terminated with exit code: {0}.", code);
}
}
break;
}
case 'object': {
// Ignore internal errors
if (exitCodeOrError.message.toString().includes('Could not find pty with id')) {
break;
}
// Convert conpty code-based failures into human friendly messages
let innerMessage = exitCodeOrError.message;
const conptyError = exitCodeOrError.message.match(/.*error code:\s*(\d+).*$/);
if (conptyError) {
const errorCode = conptyError.length > 1 ? parseInt(conptyError[1]) : undefined;
switch (errorCode) {
case 5:
innerMessage = `Access was denied to the path containing your executable "${shellLaunchConfig.executable}". Manage and change your permissions to get this to work`;
break;
case 267:
innerMessage = `Invalid starting directory "${initialCwd}", review your terminal.integrated.cwd setting`;
break;
case 1260:
innerMessage = `Windows cannot open this program because it has been prevented by a software restriction policy. For more information, open Event Viewer or contact your system Administrator`;
break;
}
}
message = nls.localize('launchFailed.errorMessage', "The terminal process failed to launch: {0}.", innerMessage);
break;
}
}
return { code, message };
}
export class TerminalInstanceColorProvider implements IXtermColorProvider {
constructor(
private readonly _target: IReference<TerminalLocation | undefined>,
@IViewDescriptorService private readonly _viewDescriptorService: IViewDescriptorService,
) {
}
getBackgroundColor(theme: IColorTheme) {
const terminalBackground = theme.getColor(TERMINAL_BACKGROUND_COLOR);
if (terminalBackground) {
return terminalBackground;
}
if (this._target.object === TerminalLocation.Editor) {
return theme.getColor(editorBackground);
}
const location = this._viewDescriptorService.getViewLocationById(TERMINAL_VIEW_ID)!;
if (location === ViewContainerLocation.Panel) {
return theme.getColor(PANEL_BACKGROUND);
}
return theme.getColor(SIDE_BAR_BACKGROUND);
}
}
function guessShellTypeFromExecutable(os: OperatingSystem, executable: string): TerminalShellType | undefined {
const exeBasename = path.basename(executable);
const generalShellTypeMap: Map<TerminalShellType, RegExp> = new Map([
[GeneralShellType.Julia, /^julia$/],
[GeneralShellType.NuShell, /^nu$/],
[GeneralShellType.PowerShell, /^pwsh(-preview)?|powershell$/],
[GeneralShellType.Python, /^py(?:thon)?$/]
]);
for (const [shellType, pattern] of generalShellTypeMap) {
if (exeBasename.match(pattern)) {
return shellType;
}
}
if (os === OperatingSystem.Windows) {
const windowsShellTypeMap: Map<TerminalShellType, RegExp> = new Map([
[WindowsShellType.CommandPrompt, /^cmd$/],
[WindowsShellType.GitBash, /^bash$/],
[WindowsShellType.Wsl, /^wsl$/]
]);
for (const [shellType, pattern] of windowsShellTypeMap) {
if (exeBasename.match(pattern)) {
return shellType;
}
}
} else {
const posixShellTypes: PosixShellType[] = [
PosixShellType.Bash,
PosixShellType.Csh,
PosixShellType.Fish,
PosixShellType.Ksh,
PosixShellType.Sh,
PosixShellType.Zsh,
];
for (const type of posixShellTypes) {
if (exeBasename === type) {
return type;
}
}
}
return undefined;
}