Merge from vscode 52dcb723a39ae75bee1bd56b3312d7fcdc87aeed (#6719)

This commit is contained in:
Anthony Dresser
2019-08-12 21:31:51 -07:00
committed by GitHub
parent 00250839fc
commit 7eba8c4c03
616 changed files with 9472 additions and 7087 deletions

View File

@@ -9,7 +9,7 @@ import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { debounce } from 'vs/base/common/decorators';
import { Emitter, Event } from 'vs/base/common/event';
import { KeyCode } from 'vs/base/common/keyCodes';
import * as lifecycle from 'vs/base/common/lifecycle';
import { IDisposable, dispose, Disposable } from 'vs/base/common/lifecycle';
import * as platform from 'vs/base/common/platform';
import { TabFocus } from 'vs/editor/common/config/commonEditorConfig';
import * as nls from 'vs/nls';
@@ -30,13 +30,13 @@ import { ansiColorIdentifiers, TERMINAL_BACKGROUND_COLOR, TERMINAL_CURSOR_BACKGR
import { TERMINAL_COMMAND_ID } from 'vs/workbench/contrib/terminal/common/terminalCommands';
import { TerminalConfigHelper } from 'vs/workbench/contrib/terminal/browser/terminalConfigHelper';
import { TerminalLinkHandler } from 'vs/workbench/contrib/terminal/browser/terminalLinkHandler';
import { TerminalCommandTracker } from 'vs/workbench/contrib/terminal/browser/terminalCommandTracker';
import { IPanelService } from 'vs/workbench/services/panel/common/panelService';
import { IAccessibilityService, AccessibilitySupport } from 'vs/platform/accessibility/common/accessibility';
import { ITerminalInstanceService } from 'vs/workbench/contrib/terminal/browser/terminal';
import { TerminalProcessManager } from 'vs/workbench/contrib/terminal/browser/terminalProcessManager';
import { Terminal as XTermTerminal, IBuffer, ITerminalAddon } from 'xterm';
import { SearchAddon, ISearchOptions } from 'xterm-addon-search';
import { CommandTrackerAddon } from 'vs/workbench/contrib/terminal/browser/addons/commandTrackerAddon';
import { NavigationModeAddon } from 'vs/workbench/contrib/terminal/browser/addons/navigationModeAddon';
// How long in milliseconds should an average frame take to render for a notification to appear
@@ -165,15 +165,15 @@ interface IGridDimensions {
rows: number;
}
export class TerminalInstance implements ITerminalInstance {
export class TerminalInstance extends Disposable implements ITerminalInstance {
private static readonly EOL_REGEX = /\r?\n/g;
private static _lastKnownCanvasDimensions: ICanvasDimensions | undefined;
private static _lastKnownGridDimensions: IGridDimensions | undefined;
private static _idCounter = 1;
private _processManager: ITerminalProcessManager | undefined;
private _pressAnyKeyToCloseListener: lifecycle.IDisposable | undefined;
private _processManager!: ITerminalProcessManager;
private _pressAnyKeyToCloseListener: IDisposable | undefined;
private _id: number;
private _isExiting: boolean;
@@ -181,27 +181,26 @@ export class TerminalInstance implements ITerminalInstance {
private _isVisible: boolean;
private _isDisposed: boolean;
private _skipTerminalCommands: string[];
private _title: string;
private _wrapperElement: HTMLDivElement;
private _xterm: XTermTerminal;
private _title: string = '';
private _wrapperElement: (HTMLElement & { xterm?: XTermTerminal }) | undefined;
private _xterm: XTermTerminal | undefined;
private _xtermSearch: SearchAddon | undefined;
private _xtermElement: HTMLDivElement;
private _xtermElement: HTMLDivElement | undefined;
private _terminalHasTextContextKey: IContextKey<boolean>;
private _terminalA11yTreeFocusContextKey: IContextKey<boolean>;
private _cols: number;
private _rows: number;
private _cols: number = 0;
private _rows: number = 0;
private _dimensionsOverride: ITerminalDimensions | undefined;
private _windowsShellHelper: IWindowsShellHelper | undefined;
private _xtermReadyPromise: Promise<void>;
private _xtermReadyPromise: Promise<XTermTerminal>;
private _titleReadyPromise: Promise<string>;
private _titleReadyComplete: (title: string) => any;
private _titleReadyComplete: ((title: string) => any) | undefined;
private readonly _disposables = new lifecycle.DisposableStore();
private _messageTitleDisposable: lifecycle.IDisposable | undefined;
private _messageTitleDisposable: IDisposable | undefined;
private _widgetManager: TerminalWidgetManager;
private _linkHandler: TerminalLinkHandler;
private _commandTracker: TerminalCommandTracker;
private _widgetManager: TerminalWidgetManager | undefined;
private _linkHandler: TerminalLinkHandler | undefined;
private _commandTrackerAddon: CommandTrackerAddon | undefined;
private _navigationModeAddon: INavigationMode & ITerminalAddon | undefined;
public disableLayout: boolean;
@@ -221,15 +220,15 @@ export class TerminalInstance implements ITerminalInstance {
public get maxCols(): number { return this._cols; }
public get maxRows(): number { return this._rows; }
// TODO: Ideally processId would be merged into processReady
public get processId(): number | undefined { return this._processManager ? this._processManager.shellProcessId : undefined; }
public 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?
public get processReady(): Promise<void> { return this._processManager ? this._processManager.ptyProcessReady : Promise.resolve(undefined); }
public get processReady(): Promise<void> { return this._processManager.ptyProcessReady; }
public get title(): string { return this._title; }
public get hadFocusOnExit(): boolean { return this._hadFocusOnExit; }
public get isTitleSetByProcess(): boolean { return !!this._messageTitleDisposable; }
public get shellLaunchConfig(): IShellLaunchConfig { return this._shellLaunchConfig; }
public get commandTracker(): TerminalCommandTracker { return this._commandTracker; }
public get commandTracker(): CommandTrackerAddon | undefined { return this._commandTrackerAddon; }
public get navigationMode(): INavigationMode | undefined { return this._navigationModeAddon; }
private readonly _onExit = new Emitter<number>();
@@ -246,8 +245,6 @@ export class TerminalInstance implements ITerminalInstance {
public get onData(): Event<string> { return this._onData.event; }
private readonly _onLineData = new Emitter<string>();
public get onLineData(): Event<string> { return this._onLineData.event; }
private readonly _onRendererInput = new Emitter<string>();
public get onRendererInput(): Event<string> { return this._onRendererInput.event; }
private readonly _onRequestExtHostProcess = new Emitter<ITerminalInstance>();
public get onRequestExtHostProcess(): Event<ITerminalInstance> { return this._onRequestExtHostProcess.event; }
private readonly _onDimensionsChanged = new Emitter<void>();
@@ -275,6 +272,8 @@ export class TerminalInstance implements ITerminalInstance {
@IStorageService private readonly _storageService: IStorageService,
@IAccessibilityService private readonly _accessibilityService: IAccessibilityService
) {
super();
this._skipTerminalCommands = [];
this._isExiting = false;
this._hadFocusOnExit = false;
@@ -293,11 +292,7 @@ export class TerminalInstance implements ITerminalInstance {
this._logService.trace(`terminalInstance#ctor (id: ${this.id})`, this._shellLaunchConfig);
this._initDimensions();
if (!this.shellLaunchConfig.isRendererOnly) {
this._createProcess();
} else {
this.setTitle(this._shellLaunchConfig.name, false);
}
this._createProcess();
this._xtermReadyPromise = this._createXterm();
this._xtermReadyPromise.then(() => {
@@ -321,8 +316,8 @@ export class TerminalInstance implements ITerminalInstance {
}));
}
public addDisposable(disposable: lifecycle.IDisposable): void {
this._disposables.add(disposable);
public addDisposable(disposable: IDisposable): void {
this._register(disposable);
}
private _initDimensions(): void {
@@ -455,11 +450,11 @@ export class TerminalInstance implements ITerminalInstance {
/**
* Create xterm.js instance and attach data listeners.
*/
protected async _createXterm(): Promise<void> {
protected async _createXterm(): Promise<XTermTerminal> {
const Terminal = await this._getXtermConstructor();
const font = this._configHelper.getFont(undefined, true);
const config = this._configHelper.config;
this._xterm = new Terminal({
const xterm = new Terminal({
scrollback: config.scrollback,
theme: this._getXtermTheme(),
drawBoldTextInBrightColors: config.drawBoldTextInBrightColors,
@@ -476,53 +471,48 @@ export class TerminalInstance implements ITerminalInstance {
// TODO: Guess whether to use canvas or dom better
rendererType: config.rendererType === 'auto' ? 'canvas' : config.rendererType
});
this._xterm = xterm;
this.updateAccessibilitySupport();
this._terminalInstanceService.getXtermSearchConstructor().then(Addon => {
this._xtermSearch = new Addon();
this._xterm.loadAddon(this._xtermSearch);
xterm.loadAddon(this._xtermSearch);
});
if (this._shellLaunchConfig.initialText) {
this._xterm.writeln(this._shellLaunchConfig.initialText);
}
this._xterm.onLineFeed(() => this._onLineFeed());
this._xterm.onKey(e => this._onKey(e.key, e.domEvent));
this._xterm.onSelectionChange(async () => this._onSelectionChange());
if (this._processManager) {
this._processManager.onProcessData(data => this._onProcessData(data));
this._xterm.onData(data => this._processManager!.write(data));
// TODO: How does the cwd work on detached processes?
this.processReady.then(async () => {
this._linkHandler.processCwd = await this._processManager!.getInitialCwd();
});
// Init winpty compat and link handler after process creation as they rely on the
// underlying process OS
this._processManager.onProcessReady(() => {
if (!this._processManager) {
return;
}
if (this._processManager.os === platform.OperatingSystem.Windows) {
this._xterm.setOption('windowsMode', true);
// Force line data to be sent when the cursor is moved, the main purpose for
// this is because ConPTY will often not do a line feed but instead move the
// cursor, in which case we still want to send the current line's data to tasks.
this._xterm.addCsiHandler('H', () => {
this._onCursorMove();
return false;
});
}
this._linkHandler = this._instantiationService.createInstance(TerminalLinkHandler, this._xterm, this._processManager);
});
} else if (this.shellLaunchConfig.isRendererOnly) {
this._linkHandler = this._instantiationService.createInstance(TerminalLinkHandler, this._xterm, undefined);
}
this._processManager.onProcessData(data => this._onProcessData(data));
this._xterm.onData(data => this._processManager.write(data));
// TODO: How does the cwd work on detached processes?
this.processReady.then(async () => {
if (this._linkHandler) {
this._linkHandler.processCwd = await this._processManager.getInitialCwd();
}
});
// Init winpty compat and link handler after process creation as they rely on the
// underlying process OS
this._processManager.onProcessReady(() => {
if (this._processManager.os === platform.OperatingSystem.Windows) {
xterm.setOption('windowsMode', true);
// Force line data to be sent when the cursor is moved, the main purpose for
// this is because ConPTY will often not do a line feed but instead move the
// cursor, in which case we still want to send the current line's data to tasks.
xterm.addCsiHandler('H', () => {
this._onCursorMove();
return false;
});
}
this._linkHandler = this._instantiationService.createInstance(TerminalLinkHandler, this._xterm, this._processManager, this._configHelper);
});
// Register listener to trigger the onInput ext API if the terminal is a renderer only
if (this._shellLaunchConfig.isRendererOnly) {
this._xterm.onData(data => this._sendRendererInput(data));
}
this._commandTrackerAddon = new CommandTrackerAddon();
this._xterm.loadAddon(this._commandTrackerAddon);
this._register(this._themeService.onThemeChange(theme => this._updateTheme(xterm, theme)));
this._commandTracker = new TerminalCommandTracker(this._xterm);
this._disposables.add(this._themeService.onThemeChange(theme => this._updateTheme(theme)));
return xterm;
}
private _isScreenReaderOptimized(): boolean {
@@ -562,7 +552,7 @@ export class TerminalInstance implements ITerminalInstance {
}
public _attachToElement(container: HTMLElement): void {
this._xtermReadyPromise.then(() => {
this._xtermReadyPromise.then(xterm => {
if (this._wrapperElement) {
throw new Error('The terminal instance has already been attached to a container');
}
@@ -573,11 +563,11 @@ export class TerminalInstance implements ITerminalInstance {
this._xtermElement = document.createElement('div');
// Attach the xterm object to the DOM, exposing it to the smoke tests
(<any>this._wrapperElement).xterm = this._xterm;
this._wrapperElement.xterm = this._xterm;
this._xterm.open(this._xtermElement);
this._xterm.textarea.addEventListener('focus', () => this._onFocus.fire(this));
this._xterm.attachCustomKeyEventHandler((event: KeyboardEvent): boolean => {
xterm.open(this._xtermElement);
xterm.textarea.addEventListener('focus', () => this._onFocus.fire(this));
xterm.attachCustomKeyEventHandler((event: KeyboardEvent): boolean => {
// Disable all input if the terminal is exiting
if (this._isExiting) {
return false;
@@ -604,7 +594,7 @@ export class TerminalInstance implements ITerminalInstance {
return true;
});
this._disposables.add(dom.addDisposableListener(this._xterm.element, 'mousedown', () => {
this._register(dom.addDisposableListener(xterm.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(document, 'mouseup', () => {
@@ -616,17 +606,17 @@ export class TerminalInstance implements ITerminalInstance {
}));
// xterm.js currently drops selection on keyup as we need to handle this case.
this._disposables.add(dom.addDisposableListener(this._xterm.element, 'keyup', () => {
this._register(dom.addDisposableListener(xterm.element, 'keyup', () => {
// Wait until keyup has propagated through the DOM before evaluating
// the new selection state.
setTimeout(() => this._refreshSelectionContextKey(), 0);
}));
const xtermHelper: HTMLElement = <HTMLElement>this._xterm.element.querySelector('.xterm-helpers');
const xtermHelper: HTMLElement = <HTMLElement>xterm.element.querySelector('.xterm-helpers');
const focusTrap: HTMLElement = document.createElement('div');
focusTrap.setAttribute('tabindex', '0');
dom.addClass(focusTrap, 'focus-trap');
this._disposables.add(dom.addDisposableListener(focusTrap, 'focus', () => {
this._register(dom.addDisposableListener(focusTrap, 'focus', () => {
let currentElement = focusTrap;
while (!dom.hasClass(currentElement, 'part')) {
currentElement = currentElement.parentElement!;
@@ -634,20 +624,20 @@ export class TerminalInstance implements ITerminalInstance {
const hidePanelElement = <HTMLElement>currentElement.querySelector('.hide-panel-action');
hidePanelElement.focus();
}));
xtermHelper.insertBefore(focusTrap, this._xterm.textarea);
xtermHelper.insertBefore(focusTrap, xterm.textarea);
this._disposables.add(dom.addDisposableListener(this._xterm.textarea, 'focus', () => {
this._register(dom.addDisposableListener(xterm.textarea, 'focus', () => {
this._terminalFocusContextKey.set(true);
this._onFocused.fire(this);
}));
this._disposables.add(dom.addDisposableListener(this._xterm.textarea, 'blur', () => {
this._register(dom.addDisposableListener(xterm.textarea, 'blur', () => {
this._terminalFocusContextKey.reset();
this._refreshSelectionContextKey();
}));
this._disposables.add(dom.addDisposableListener(this._xterm.element, 'focus', () => {
this._register(dom.addDisposableListener(xterm.element, 'focus', () => {
this._terminalFocusContextKey.set(true);
}));
this._disposables.add(dom.addDisposableListener(this._xterm.element, 'blur', () => {
this._register(dom.addDisposableListener(xterm.element, 'blur', () => {
this._terminalFocusContextKey.reset();
this._refreshSelectionContextKey();
}));
@@ -655,13 +645,13 @@ export class TerminalInstance implements ITerminalInstance {
this._wrapperElement.appendChild(this._xtermElement);
this._container.appendChild(this._wrapperElement);
if (this._processManager) {
this._widgetManager = new TerminalWidgetManager(this._wrapperElement);
this._processManager.onProcessReady(() => this._linkHandler.setWidgetManager(this._widgetManager));
} else if (this._shellLaunchConfig.isRendererOnly) {
this._widgetManager = new TerminalWidgetManager(this._wrapperElement);
this._linkHandler.setWidgetManager(this._widgetManager);
}
const widgetManager = new TerminalWidgetManager(this._wrapperElement);
this._widgetManager = widgetManager;
this._processManager.onProcessReady(() => {
if (this._linkHandler) {
this._linkHandler.setWidgetManager(widgetManager);
}
});
const computedStyle = window.getComputedStyle(this._container);
const width = parseInt(computedStyle.getPropertyValue('width').replace('px', ''), 10);
@@ -672,8 +662,8 @@ export class TerminalInstance implements ITerminalInstance {
// If IShellLaunchConfig.waitOnExit was true and the process finished before the terminal
// panel was initialized.
if (this._xterm.getOption('disableStdin')) {
this._attachPressAnyKeyToCloseListener();
if (xterm.getOption('disableStdin')) {
this._attachPressAnyKeyToCloseListener(xterm);
}
const neverMeasureRenderTime = this._storageService.getBoolean(NEVER_MEASURE_RENDER_TIME_STORAGE_KEY, StorageScope.GLOBAL, false);
@@ -683,9 +673,10 @@ export class TerminalInstance implements ITerminalInstance {
});
}
private _measureRenderTime(): void {
private async _measureRenderTime(): Promise<void> {
const xterm = await this._xtermReadyPromise;
const frameTimes: number[] = [];
const textRenderLayer = this._xterm._core._renderService._renderer._renderLayers[0];
const textRenderLayer = xterm._core._renderService._renderer._renderLayers[0];
const originalOnGridChanged = textRenderLayer.onGridChanged;
const evaluateCanvasRenderer = () => {
@@ -730,34 +721,41 @@ export class TerminalInstance implements ITerminalInstance {
}
public registerLinkMatcher(regex: RegExp, handler: (url: string) => void, matchIndex?: number, validationCallback?: (uri: string, callback: (isValid: boolean) => void) => void): number {
return this._linkHandler.registerCustomLinkHandler(regex, handler, matchIndex, validationCallback);
return this._linkHandler!.registerCustomLinkHandler(regex, handler, matchIndex, validationCallback);
}
public deregisterLinkMatcher(linkMatcherId: number): void {
this._xterm.deregisterLinkMatcher(linkMatcherId);
this._xtermReadyPromise.then(xterm => xterm.deregisterLinkMatcher(linkMatcherId));
}
public hasSelection(): boolean {
return this._xterm && this._xterm.hasSelection();
return this._xterm ? this._xterm.hasSelection() : false;
}
public async copySelection(): Promise<void> {
const xterm = await this._xtermReadyPromise;
if (this.hasSelection()) {
await this._clipboardService.writeText(this._xterm.getSelection());
await this._clipboardService.writeText(xterm.getSelection());
} else {
this._notificationService.warn(nls.localize('terminal.integrated.copySelection.noSelection', 'The terminal has no selection to copy'));
}
}
public get selection(): string | undefined {
return this.hasSelection() ? this._xterm.getSelection() : undefined;
return this._xterm && this.hasSelection() ? this._xterm.getSelection() : undefined;
}
public clearSelection(): void {
if (!this._xterm) {
return;
}
this._xterm.clearSelection();
}
public selectAll(): void {
if (!this._xterm) {
return;
}
// Focus here to ensure the terminal context key is set
this._xterm.focus();
this._xterm.selectAll();
@@ -778,6 +776,9 @@ export class TerminalInstance implements ITerminalInstance {
}
public notifyFindWidgetFocusChanged(isFocused: boolean): void {
if (!this._xterm) {
return;
}
const terminalFocused = !isFocused && (document.activeElement === this._xterm.textarea || document.activeElement === this._xterm.element);
this._terminalFocusContextKey.set(terminalFocused);
}
@@ -785,18 +786,18 @@ export class TerminalInstance implements ITerminalInstance {
public dispose(immediate?: boolean): void {
this._logService.trace(`terminalInstance#dispose (id: ${this.id})`);
lifecycle.dispose(this._windowsShellHelper);
dispose(this._windowsShellHelper);
this._windowsShellHelper = undefined;
this._linkHandler = lifecycle.dispose(this._linkHandler);
this._commandTracker = lifecycle.dispose(this._commandTracker);
this._widgetManager = lifecycle.dispose(this._widgetManager);
this._linkHandler = dispose(this._linkHandler);
this._commandTrackerAddon = dispose(this._commandTrackerAddon);
this._widgetManager = dispose(this._widgetManager);
if (this._xterm && this._xterm.element) {
this._hadFocusOnExit = dom.hasClass(this._xterm.element, 'focus');
}
if (this._wrapperElement) {
if ((<any>this._wrapperElement).xterm) {
(<any>this._wrapperElement).xterm = null;
if (this._wrapperElement.xterm) {
this._wrapperElement.xterm = undefined;
}
if (this._wrapperElement.parentElement) {
this._container.removeChild(this._wrapperElement);
@@ -813,39 +814,29 @@ export class TerminalInstance implements ITerminalInstance {
this._pressAnyKeyToCloseListener = undefined;
}
if (this._processManager) {
this._processManager.dispose(immediate);
} else {
// In cases where there is no associated process (for example executing an extension callback task)
// consumers still expect on onExit event to be fired. An example of this is terminating the extension callback
// task.
this._onExit.fire(0);
}
this._processManager.dispose(immediate);
// Process manager dispose/shutdown doesn't fire process exit, trigger with undefined if it
// hasn't happened yet
this._onProcessExit(undefined);
if (!this._isDisposed) {
this._isDisposed = true;
this._onDisposed.fire(this);
}
this._disposables.dispose();
}
public rendererExit(exitCode: number): void {
// The use of this API is for cases where there is no backing process behind a terminal
// instance (e.g. a custom execution task).
if (!this.shellLaunchConfig.isRendererOnly) {
throw new Error('rendererExit is only expected to be called on a renderer only terminal');
}
return this._onProcessExit(exitCode);
super.dispose();
}
public forceRedraw(): void {
if (!this._xterm) {
return;
}
if (this._configHelper.config.experimentalRefreshOnResume) {
if (this._xterm.getOption('rendererType') !== 'dom') {
this._xterm.setOption('rendererType', 'dom');
// Do this asynchronously to clear our the texture atlas as all terminals will not
// be using canvas
setTimeout(() => this._xterm.setOption('rendererType', 'canvas'), 0);
const xterm = this._xterm;
setTimeout(() => xterm.setOption('rendererType', 'canvas'), 0);
}
}
this._xterm.refresh(0, this._xterm.rows - 1);
@@ -870,6 +861,9 @@ export class TerminalInstance implements ITerminalInstance {
}
public async paste(): Promise<void> {
if (!this._xterm) {
return;
}
this.focus();
this._xterm._core._coreService.triggerDataEvent(await this._clipboardService.readText(), true);
}
@@ -880,10 +874,6 @@ export class TerminalInstance implements ITerminalInstance {
return;
}
this._xterm.write(text);
if (this._shellLaunchConfig.isRendererOnly) {
// Fire onData API in the extension host
this._onData.fire(text);
}
});
}
@@ -894,17 +884,8 @@ export class TerminalInstance implements ITerminalInstance {
text += '\r';
}
if (this._shellLaunchConfig.isRendererOnly) {
// If the terminal is a renderer only, fire the onInput ext API
this._sendRendererInput(text);
} else {
// If the terminal has a process, send it to the process
if (this._processManager) {
this._processManager.ptyProcessReady.then(() => {
this._processManager!.write(text);
});
}
}
// Send it to the process
this._processManager.ptyProcessReady.then(() => this._processManager.write(text));
}
public setVisible(visible: boolean): void {
@@ -936,31 +917,45 @@ export class TerminalInstance implements ITerminalInstance {
}
public scrollDownLine(): void {
this._xterm.scrollLines(1);
if (this._xterm) {
this._xterm.scrollLines(1);
}
}
public scrollDownPage(): void {
this._xterm.scrollPages(1);
if (this._xterm) {
this._xterm.scrollPages(1);
}
}
public scrollToBottom(): void {
this._xterm.scrollToBottom();
if (this._xterm) {
this._xterm.scrollToBottom();
}
}
public scrollUpLine(): void {
this._xterm.scrollLines(-1);
if (this._xterm) {
this._xterm.scrollLines(-1);
}
}
public scrollUpPage(): void {
this._xterm.scrollPages(-1);
if (this._xterm) {
this._xterm.scrollPages(-1);
}
}
public scrollToTop(): void {
this._xterm.scrollToTop();
if (this._xterm) {
this._xterm.scrollToTop();
}
}
public clear(): void {
this._xterm.clear();
if (this._xterm) {
this._xterm.clear();
}
}
private _refreshSelectionContextKey() {
@@ -987,12 +982,12 @@ export class TerminalInstance implements ITerminalInstance {
if (platform.isWindows) {
this._processManager.ptyProcessReady.then(() => {
if (this._processManager!.remoteAuthority) {
if (this._processManager.remoteAuthority) {
return;
}
this._xtermReadyPromise.then(() => {
if (!this._isDisposed) {
this._windowsShellHelper = this._terminalInstanceService.createWindowsShellHelper(this._processManager!.shellProcessId, this, this._xterm);
this._xtermReadyPromise.then(xterm => {
if (!this._isDisposed && this._processManager && this._processManager.shellProcessId) {
this._windowsShellHelper = this._terminalInstanceService.createWindowsShellHelper(this._processManager.shellProcessId, this, xterm);
}
});
});
@@ -1001,7 +996,7 @@ export class TerminalInstance implements ITerminalInstance {
// Create the process asynchronously to allow the terminal's container
// to be created so dimensions are accurate
setTimeout(() => {
this._processManager!.createProcess(this._shellLaunchConfig, this._cols, this._rows, this._isScreenReaderOptimized());
this._processManager.createProcess(this._shellLaunchConfig, this._cols, this._rows, this._isScreenReaderOptimized());
}, 0);
}
@@ -1021,13 +1016,13 @@ export class TerminalInstance implements ITerminalInstance {
* through user action.
*/
private _onProcessExit(exitCode?: number): void {
this._logService.debug(`Terminal process exit (id: ${this.id}) with code ${exitCode}`);
// Prevent dispose functions being triggered multiple times
if (this._isExiting) {
return;
}
this._logService.debug(`Terminal process exit (id: ${this.id}) with code ${exitCode}`);
this._isExiting = true;
let exitCodeMessage: string | undefined;
@@ -1039,7 +1034,7 @@ export class TerminalInstance implements ITerminalInstance {
exitCodeMessage = nls.localize('terminal.integrated.exitedWithInvalidPathDirectory', 'The terminal shell path "{0}" is a directory', this._shellLaunchConfig.executable);
} else if (exitCode === SHELL_CWD_INVALID_EXIT_CODE && this._shellLaunchConfig.cwd) {
exitCodeMessage = nls.localize('terminal.integrated.exitedWithInvalidCWD', 'The terminal shell CWD "{0}" does not exist', this._shellLaunchConfig.cwd.toString());
} else if (this._processManager && this._processManager.processState === ProcessState.KILLED_DURING_LAUNCH) {
} else if (this._processManager.processState === ProcessState.KILLED_DURING_LAUNCH) {
let args = '';
if (typeof this._shellLaunchConfig.args === 'string') {
args = ` ${this._shellLaunchConfig.args}`;
@@ -1061,29 +1056,31 @@ export class TerminalInstance implements ITerminalInstance {
}
}
this._logService.debug(`Terminal process exit (id: ${this.id})${this._processManager ? ' state ' + this._processManager.processState : ''}`);
this._logService.debug(`Terminal process exit (id: ${this.id}) state ${this._processManager.processState}`);
// Only trigger wait on exit when the exit was *not* triggered by the
// user (via the `workbench.action.terminal.kill` command).
if (this._shellLaunchConfig.waitOnExit && (!this._processManager || this._processManager.processState !== ProcessState.KILLED_BY_USER)) {
if (exitCodeMessage) {
this._xterm.writeln(exitCodeMessage);
}
if (typeof this._shellLaunchConfig.waitOnExit === 'string') {
let message = this._shellLaunchConfig.waitOnExit;
// Bold the message and add an extra new line to make it stand out from the rest of the output
message = `\r\n\x1b[1m${message}\x1b[0m`;
this._xterm.writeln(message);
}
// Disable all input if the terminal is exiting and listen for next keypress
this._xterm.setOption('disableStdin', true);
if (this._xterm.textarea) {
this._attachPressAnyKeyToCloseListener();
}
if (this._shellLaunchConfig.waitOnExit && this._processManager.processState !== ProcessState.KILLED_BY_USER) {
this._xtermReadyPromise.then(xterm => {
if (exitCodeMessage) {
xterm.writeln(exitCodeMessage);
}
if (typeof this._shellLaunchConfig.waitOnExit === 'string') {
let message = this._shellLaunchConfig.waitOnExit;
// Bold the message and add an extra new line to make it stand out from the rest of the output
message = `\r\n\x1b[1m${message}\x1b[0m`;
xterm.writeln(message);
}
// Disable all input if the terminal is exiting and listen for next keypress
xterm.setOption('disableStdin', true);
if (xterm.textarea) {
this._attachPressAnyKeyToCloseListener(xterm);
}
});
} else {
this.dispose();
if (exitCodeMessage) {
if (this._processManager && this._processManager.processState === ProcessState.KILLED_DURING_LAUNCH) {
if (this._processManager.processState === ProcessState.KILLED_DURING_LAUNCH) {
this._notificationService.error(exitCodeMessage);
} else {
if (this._configHelper.config.showExitAlert) {
@@ -1098,9 +1095,9 @@ export class TerminalInstance implements ITerminalInstance {
this._onExit.fire(exitCode || 0);
}
private _attachPressAnyKeyToCloseListener() {
private _attachPressAnyKeyToCloseListener(xterm: XTermTerminal) {
if (!this._pressAnyKeyToCloseListener) {
this._pressAnyKeyToCloseListener = dom.addDisposableListener(this._xterm.textarea, 'keypress', (event: KeyboardEvent) => {
this._pressAnyKeyToCloseListener = dom.addDisposableListener(xterm.textarea, 'keypress', (event: KeyboardEvent) => {
if (this._pressAnyKeyToCloseListener) {
this._pressAnyKeyToCloseListener.dispose();
this._pressAnyKeyToCloseListener = undefined;
@@ -1119,59 +1116,47 @@ export class TerminalInstance implements ITerminalInstance {
}
// Kill and clear up the process, making the process manager ready for a new process
if (this._processManager) {
this._processManager.dispose();
this._processManager = undefined;
this._processManager.dispose();
if (this._xterm) {
// Ensure new processes' output starts at start of new line
this._xterm.write('\n\x1b[G');
// Print initialText if specified
if (shell.initialText) {
this._xterm.writeln(shell.initialText);
}
// Clean up waitOnExit state
if (this._isExiting && this._shellLaunchConfig.waitOnExit) {
this._xterm.setOption('disableStdin', false);
this._isExiting = false;
}
}
// Ensure new processes' output starts at start of new line
this._xterm.write('\n\x1b[G');
// Print initialText if specified
if (shell.initialText) {
this._xterm.writeln(shell.initialText);
}
const oldTitle = this._title;
// Clean up waitOnExit state
if (this._isExiting && this._shellLaunchConfig.waitOnExit) {
this._xterm.setOption('disableStdin', false);
this._isExiting = false;
}
// 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 hang
// 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()
// Launch the process unless this is only a renderer.
// In the renderer only cases, we still need to set the title correctly.
if (!this._shellLaunchConfig.isRendererOnly) {
this._createProcess();
} else if (this._shellLaunchConfig.name) {
this.setTitle(this._shellLaunchConfig.name, false);
}
const oldTitle = this._title;
this._createProcess();
if (oldTitle !== this._title) {
this.setTitle(this._title, true);
}
if (this._processManager) {
// The "!" operator is required here because _processManager is set to undefiend earlier
// and TS does not know that createProcess sets it.
this._processManager!.onProcessData(data => this._onProcessData(data));
}
}
private _sendRendererInput(input: string): void {
if (this._processManager) {
throw new Error('onRendererInput attempted to be used on a regular terminal');
}
// For terminal renderers onData fires on keystrokes and when sendText is called.
this._onRendererInput.fire(input);
this._processManager.onProcessData(data => this._onProcessData(data));
}
private _onLineFeed(): void {
const buffer = this._xterm.buffer;
const buffer = this._xterm!.buffer;
const newLine = buffer.getLine(buffer.baseY + buffer.cursorY);
if (newLine && !newLine.isWrapped) {
this._sendLineData(buffer, buffer.baseY + buffer.cursorY - 1);
@@ -1179,7 +1164,7 @@ export class TerminalInstance implements ITerminalInstance {
}
private _onCursorMove(): void {
const buffer = this._xterm.buffer;
const buffer = this._xterm!.buffer;
this._sendLineData(buffer, buffer.baseY + buffer.cursorY);
}
@@ -1207,11 +1192,19 @@ export class TerminalInstance implements ITerminalInstance {
}
}
private async _onSelectionChange(): Promise<void> {
if (this._configurationService.getValue('terminal.integrated.copyOnSelection')) {
if (this.hasSelection()) {
await this.copySelection();
}
}
}
@debounce(2000)
private async _updateProcessCwd(): Promise<string> {
// reset cwd if it has changed, so file based url paths can be resolved
const cwd = await this.getCwd();
if (cwd) {
if (cwd && this._linkHandler) {
this._linkHandler.processCwd = cwd;
}
return cwd;
@@ -1234,14 +1227,14 @@ export class TerminalInstance implements ITerminalInstance {
const isEnabled = this._isScreenReaderOptimized();
if (isEnabled) {
this._navigationModeAddon = new NavigationModeAddon(this._terminalA11yTreeFocusContextKey);
this._xterm.loadAddon(this._navigationModeAddon);
this._xterm!.loadAddon(this._navigationModeAddon);
} else {
if (this._navigationModeAddon) {
this._navigationModeAddon.dispose();
this._navigationModeAddon = undefined;
}
}
this._xterm.setOption('screenReaderMode', isEnabled);
this._xterm!.setOption('screenReaderMode', isEnabled);
}
private _setCursorBlink(blink: boolean): void {
@@ -1352,9 +1345,7 @@ export class TerminalInstance implements ITerminalInstance {
}
}
if (this._processManager) {
this._processManager.ptyProcessReady.then(() => this._processManager!.setDimensions(cols, rows));
}
this._processManager.ptyProcessReady.then(() => this._processManager.setDimensions(cols, rows));
}
public setTitle(title: string | undefined, eventFromProcess: boolean): void {
@@ -1370,19 +1361,17 @@ export class TerminalInstance implements ITerminalInstance {
} else {
// If the title has not been set by the API or the rename command, unregister the handler that
// automatically updates the terminal name
if (this._messageTitleDisposable) {
lifecycle.dispose(this._messageTitleDisposable);
lifecycle.dispose(this._windowsShellHelper);
this._messageTitleDisposable = undefined;
this._windowsShellHelper = undefined;
}
dispose(this._messageTitleDisposable);
this._messageTitleDisposable = undefined;
dispose(this._windowsShellHelper);
this._windowsShellHelper = undefined;
}
const didTitleChange = title !== this._title;
const oldTitle = this._title;
this._title = title;
if (didTitleChange) {
if (!oldTitle) {
if (this._titleReadyComplete) {
this._titleReadyComplete(title);
this._titleReadyComplete = undefined;
}
this._onTitleChanged.fire(this);
}
@@ -1440,25 +1429,21 @@ export class TerminalInstance implements ITerminalInstance {
};
}
private _updateTheme(theme?: ITheme): void {
this._xterm.setOption('theme', this._getXtermTheme(theme));
private _updateTheme(xterm: XTermTerminal, theme?: ITheme): void {
xterm.setOption('theme', this._getXtermTheme(theme));
}
public toggleEscapeSequenceLogging(): void {
this._xterm.setOption('logLevel', 'debug');
public async toggleEscapeSequenceLogging(): Promise<void> {
const xterm = await this._xtermReadyPromise;
const isDebug = xterm.getOption('logLevel') === 'debug';
xterm.setOption('logLevel', isDebug ? 'info' : 'debug');
}
public getInitialCwd(): Promise<string> {
if (!this._processManager) {
return Promise.resolve('');
}
return this._processManager.getInitialCwd();
}
public getCwd(): Promise<string> {
if (!this._processManager) {
return Promise.resolve('');
}
return this._processManager.getCwd();
}
}