diff --git a/src/vs/workbench/api/node/extHostTerminalService.ts b/src/vs/workbench/api/node/extHostTerminalService.ts index f373f4c08ed..4462de7b458 100644 --- a/src/vs/workbench/api/node/extHostTerminalService.ts +++ b/src/vs/workbench/api/node/extHostTerminalService.ts @@ -27,9 +27,9 @@ import { IDisposable } from 'vs/base/common/lifecycle'; const RENDERER_NO_PROCESS_ID = -1; export class BaseExtHostTerminal { - public _id: number; + public _id: number | undefined; protected _idPromise: Promise; - private _idPromiseComplete: (value: number) => any; + private _idPromiseComplete: ((value: number) => any) | undefined; private _disposed: boolean = false; private _queuedRequests: ApiRequest[] = []; @@ -71,9 +71,12 @@ export class BaseExtHostTerminal { public _runQueuedRequests(id: number): void { this._id = id; - this._idPromiseComplete(id); + if (this._idPromiseComplete) { + this._idPromiseComplete(id); + this._idPromiseComplete = undefined; + } this._queuedRequests.forEach((r) => { - r.run(this._proxy, this._id); + r.run(this._proxy, id); }); this._queuedRequests.length = 0; } @@ -82,14 +85,14 @@ export class BaseExtHostTerminal { export class ExtHostTerminal extends BaseExtHostTerminal implements vscode.Terminal { private _pidPromise: Promise; private _cols: number | undefined; - private _pidPromiseComplete: ((value: number | undefined) => any) | null; + private _pidPromiseComplete: ((value: number | undefined) => any) | undefined; private _rows: number | undefined; private readonly _onData = new Emitter(); public get onDidWriteData(): Event { // Tell the main side to start sending data if it's not already - this._idPromise.then(c => { - this._proxy.$registerOnDataListener(this._id); + this._idPromise.then(id => { + this._proxy.$registerOnDataListener(id); }); return this._onData.event; } @@ -124,10 +127,11 @@ export class ExtHostTerminal extends BaseExtHostTerminal implements vscode.Termi this._runQueuedRequests(terminal.id); } - public async createExtensionTerminal(): Promise { + public async createExtensionTerminal(): Promise { const terminal = await this._proxy.$createTerminal({ name: this._name, isExtensionTerminal: true }); this._name = terminal.name; this._runQueuedRequests(terminal.id); + return terminal.id; } public get name(): string { @@ -181,7 +185,7 @@ export class ExtHostTerminal extends BaseExtHostTerminal implements vscode.Termi // The event may fire 2 times when the panel is restored if (this._pidPromiseComplete) { this._pidPromiseComplete(processId); - this._pidPromiseComplete = null; + this._pidPromiseComplete = undefined; } else { // Recreate the promise if this is the nth processId set (e.g. reused task terminals) this._pidPromise.then(pid => { @@ -332,7 +336,7 @@ export class ExtHostTerminalService implements ExtHostTerminalServiceShape { public createExtensionTerminal(options: vscode.ExtensionTerminalOptions): vscode.Terminal { const terminal = new ExtHostTerminal(this._proxy, options.name); const p = new ExtHostPseudoterminal(options.pty); - terminal.createExtensionTerminal().then(() => this._setupExtHostProcessListeners(terminal._id, p)); + terminal.createExtensionTerminal().then(id => this._setupExtHostProcessListeners(id, p)); this._terminals.push(terminal); return terminal; } diff --git a/src/vs/workbench/contrib/terminal/browser/addons/navigationModeAddon.ts b/src/vs/workbench/contrib/terminal/browser/addons/navigationModeAddon.ts index 74542173298..6997a4dbef1 100644 --- a/src/vs/workbench/contrib/terminal/browser/addons/navigationModeAddon.ts +++ b/src/vs/workbench/contrib/terminal/browser/addons/navigationModeAddon.ts @@ -9,7 +9,7 @@ import { addDisposableListener } from 'vs/base/browser/dom'; import { INavigationMode } from 'vs/workbench/contrib/terminal/common/terminal'; export class NavigationModeAddon implements INavigationMode, ITerminalAddon { - private _terminal: Terminal; + private _terminal: Terminal | undefined; constructor( private _navigationModeContextKey: IContextKey @@ -22,11 +22,18 @@ export class NavigationModeAddon implements INavigationMode, ITerminalAddon { dispose() { } exitNavigationMode(): void { + if (!this._terminal) { + return; + } this._terminal.scrollToBottom(); this._terminal.focus(); } focusPreviousLine(): void { + if (!this._terminal) { + return; + } + // Focus previous row if a row is already focused if (document.activeElement && document.activeElement.parentElement && document.activeElement.parentElement.classList.contains('xterm-accessibility-tree')) { const element = document.activeElement.previousElementSibling; @@ -66,6 +73,10 @@ export class NavigationModeAddon implements INavigationMode, ITerminalAddon { } focusNextLine(): void { + if (!this._terminal) { + return; + } + // Focus previous row if a row is already focused if (document.activeElement && document.activeElement.parentElement && document.activeElement.parentElement.classList.contains('xterm-accessibility-tree')) { const element = document.activeElement.nextElementSibling; @@ -103,4 +114,4 @@ export class NavigationModeAddon implements INavigationMode, ITerminalAddon { }); this._navigationModeContextKey.set(true); } -} \ No newline at end of file +} diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index 411d6bf2c29..c11888713b2 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -181,9 +181,9 @@ 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 _terminalHasTextContextKey: IContextKey; @@ -192,7 +192,7 @@ export class TerminalInstance implements ITerminalInstance { private _rows: number; private _dimensionsOverride: ITerminalDimensions | undefined; private _windowsShellHelper: IWindowsShellHelper | undefined; - private _xtermReadyPromise: Promise; + private _xtermReadyPromise: Promise; private _titleReadyPromise: Promise; private _titleReadyComplete: (title: string) => any; @@ -455,11 +455,11 @@ export class TerminalInstance implements ITerminalInstance { /** * Create xterm.js instance and attach data listeners. */ - protected async _createXterm(): Promise { + protected async _createXterm(): Promise { 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,10 +476,11 @@ 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); @@ -501,11 +502,11 @@ export class TerminalInstance implements ITerminalInstance { return; } if (this._processManager.os === platform.OperatingSystem.Windows) { - this._xterm.setOption('windowsMode', true); + 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', () => { + xterm.addCsiHandler('H', () => { this._onCursorMove(); return false; }); @@ -522,7 +523,9 @@ export class TerminalInstance implements ITerminalInstance { } this._commandTracker = new TerminalCommandTracker(this._xterm); - this._disposables.add(this._themeService.onThemeChange(theme => this._updateTheme(theme))); + this._disposables.add(this._themeService.onThemeChange(theme => this._updateTheme(xterm, theme))); + + return xterm; } private _isScreenReaderOptimized(): boolean { @@ -562,7 +565,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 +576,11 @@ export class TerminalInstance implements ITerminalInstance { this._xtermElement = document.createElement('div'); // Attach the xterm object to the DOM, exposing it to the smoke tests - (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 +607,7 @@ export class TerminalInstance implements ITerminalInstance { return true; }); - this._disposables.add(dom.addDisposableListener(this._xterm.element, 'mousedown', () => { + this._disposables.add(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,13 +619,13 @@ 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._disposables.add(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 = this._xterm.element.querySelector('.xterm-helpers'); + const xtermHelper: HTMLElement = xterm.element.querySelector('.xterm-helpers'); const focusTrap: HTMLElement = document.createElement('div'); focusTrap.setAttribute('tabindex', '0'); dom.addClass(focusTrap, 'focus-trap'); @@ -634,20 +637,20 @@ export class TerminalInstance implements ITerminalInstance { const hidePanelElement = 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._disposables.add(dom.addDisposableListener(xterm.textarea, 'focus', () => { this._terminalFocusContextKey.set(true); this._onFocused.fire(this); })); - this._disposables.add(dom.addDisposableListener(this._xterm.textarea, 'blur', () => { + this._disposables.add(dom.addDisposableListener(xterm.textarea, 'blur', () => { this._terminalFocusContextKey.reset(); this._refreshSelectionContextKey(); })); - this._disposables.add(dom.addDisposableListener(this._xterm.element, 'focus', () => { + this._disposables.add(dom.addDisposableListener(xterm.element, 'focus', () => { this._terminalFocusContextKey.set(true); })); - this._disposables.add(dom.addDisposableListener(this._xterm.element, 'blur', () => { + this._disposables.add(dom.addDisposableListener(xterm.element, 'blur', () => { this._terminalFocusContextKey.reset(); this._refreshSelectionContextKey(); })); @@ -672,8 +675,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 +686,10 @@ export class TerminalInstance implements ITerminalInstance { }); } - private _measureRenderTime(): void { + private async _measureRenderTime(): Promise { + 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 = () => { @@ -734,30 +738,37 @@ export class TerminalInstance implements ITerminalInstance { } 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 { + 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 +789,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); } @@ -795,8 +809,8 @@ export class TerminalInstance implements ITerminalInstance { this._hadFocusOnExit = dom.hasClass(this._xterm.element, 'focus'); } if (this._wrapperElement) { - if ((this._wrapperElement).xterm) { - (this._wrapperElement).xterm = null; + if (this._wrapperElement.xterm) { + this._wrapperElement.xterm = undefined; } if (this._wrapperElement.parentElement) { this._container.removeChild(this._wrapperElement); @@ -840,12 +854,16 @@ export class TerminalInstance implements ITerminalInstance { } 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 +888,9 @@ export class TerminalInstance implements ITerminalInstance { } public async paste(): Promise { + if (!this._xterm) { + return; + } this.focus(); this._xterm._core._coreService.triggerDataEvent(await this._clipboardService.readText(), true); } @@ -936,31 +957,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() { @@ -990,9 +1025,9 @@ export class TerminalInstance implements ITerminalInstance { 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); } }); }); @@ -1066,20 +1101,22 @@ export class TerminalInstance implements ITerminalInstance { // 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(); - } + 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) { @@ -1098,9 +1135,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; @@ -1124,19 +1161,20 @@ export class TerminalInstance implements ITerminalInstance { this._processManager = undefined; } - // Ensure new processes' output starts at start of new line - this._xterm.write('\n\x1b[G'); + 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); - } + // 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; + // Clean up waitOnExit state + if (this._isExiting && this._shellLaunchConfig.waitOnExit) { + this._xterm.setOption('disableStdin', false); + this._isExiting = false; + } } // Set the new shell launch config @@ -1144,6 +1182,7 @@ export class TerminalInstance implements ITerminalInstance { // Launch the process unless this is only a renderer. // In the renderer only cases, we still need to set the title correctly. + const oldTitle = this._title; if (!this._shellLaunchConfig.isRendererOnly) { this._createProcess(); } else if (this._shellLaunchConfig.name) { @@ -1171,7 +1210,7 @@ export class TerminalInstance implements ITerminalInstance { } 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 +1218,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); } @@ -1234,14 +1273,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 { @@ -1440,13 +1479,14 @@ 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 { - const isDebug = this._xterm.getOption('logLevel') === 'debug'; - this._xterm.setOption('logLevel', isDebug ? 'info' : 'debug'); + public async toggleEscapeSequenceLogging(): Promise { + const xterm = await this._xtermReadyPromise; + const isDebug = xterm.getOption('logLevel') === 'debug'; + xterm.setOption('logLevel', isDebug ? 'info' : 'debug'); } public getInitialCwd(): Promise { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalLinkHandler.ts b/src/vs/workbench/contrib/terminal/browser/terminalLinkHandler.ts index 1313dabefa6..938ab02e434 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalLinkHandler.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalLinkHandler.ts @@ -68,8 +68,8 @@ interface IPath { export class TerminalLinkHandler { private readonly _hoverDisposables = new DisposableStore(); private _mouseMoveDisposable: IDisposable; - private _widgetManager: TerminalWidgetManager; - private _processCwd: string; + private _widgetManager: TerminalWidgetManager | undefined; + private _processCwd: string | undefined; private _gitDiffPreImagePattern: RegExp; private _gitDiffPostImagePattern: RegExp; private readonly _tooltipCallback: (event: MouseEvent, uri: string) => boolean | void; diff --git a/src/vs/workbench/contrib/terminal/browser/terminalPanel.ts b/src/vs/workbench/contrib/terminal/browser/terminalPanel.ts index c79e4eedcb0..77d6388d0ef 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalPanel.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalPanel.ts @@ -29,14 +29,14 @@ const FIND_FOCUS_CLASS = 'find-focused'; export class TerminalPanel extends Panel { - private _actions: IAction[]; - private _copyContextMenuAction: IAction; - private _contextMenuActions: IAction[]; + private _actions: IAction[] | undefined; + private _copyContextMenuAction: IAction | undefined; + private _contextMenuActions: IAction[] | undefined; private _cancelContextMenu: boolean = false; - private _fontStyleElement: HTMLElement; - private _parentDomElement: HTMLElement; - private _terminalContainer: HTMLElement; - private _findWidget: TerminalFindWidget; + private _fontStyleElement: HTMLElement | undefined; + private _parentDomElement: HTMLElement | undefined; + private _terminalContainer: HTMLElement | undefined; + private _findWidget: TerminalFindWidget | undefined; constructor( @IConfigurationService private readonly _configurationService: IConfigurationService, @@ -61,14 +61,13 @@ export class TerminalPanel extends Panel { dom.addClass(this._terminalContainer, 'terminal-outer-container'); this._findWidget = this._instantiationService.createInstance(TerminalFindWidget, this._terminalService.getFindState()); - this._findWidget.focusTracker.onDidFocus(() => this._terminalContainer.classList.add(FIND_FOCUS_CLASS)); - this._findWidget.focusTracker.onDidBlur(() => this._terminalContainer.classList.remove(FIND_FOCUS_CLASS)); + this._findWidget.focusTracker.onDidFocus(() => this._terminalContainer!.classList.add(FIND_FOCUS_CLASS)); this._parentDomElement.appendChild(this._fontStyleElement); this._parentDomElement.appendChild(this._terminalContainer); this._parentDomElement.appendChild(this._findWidget.getDomNode()); - this._attachEventListeners(); + this._attachEventListeners(this._parentDomElement, this._terminalContainer); this._terminalService.setContainers(this.getContainer(), this._terminalContainer); @@ -137,7 +136,7 @@ export class TerminalPanel extends Panel { } private _getContextMenuActions(): IAction[] { - if (!this._contextMenuActions) { + if (!this._contextMenuActions || !this._copyContextMenuAction) { this._copyContextMenuAction = this._instantiationService.createInstance(CopyTerminalSelectionAction, CopyTerminalSelectionAction.ID, CopyTerminalSelectionAction.SHORT_LABEL); this._contextMenuActions = [ this._instantiationService.createInstance(CreateNewTerminalAction, CreateNewTerminalAction.ID, CreateNewTerminalAction.SHORT_LABEL), @@ -179,31 +178,31 @@ export class TerminalPanel extends Panel { public focusFindWidget() { const activeInstance = this._terminalService.getActiveInstance(); if (activeInstance && activeInstance.hasSelection() && activeInstance.selection!.indexOf('\n') === -1) { - this._findWidget.reveal(activeInstance.selection); + this._findWidget!.reveal(activeInstance.selection); } else { - this._findWidget.reveal(); + this._findWidget!.reveal(); } } public hideFindWidget() { - this._findWidget.hide(); + this._findWidget!.hide(); } public showFindWidget() { const activeInstance = this._terminalService.getActiveInstance(); if (activeInstance && activeInstance.hasSelection() && activeInstance.selection!.indexOf('\n') === -1) { - this._findWidget.show(activeInstance.selection); + this._findWidget!.show(activeInstance.selection); } else { - this._findWidget.show(); + this._findWidget!.show(); } } public getFindWidget(): TerminalFindWidget { - return this._findWidget; + return this._findWidget!; } - private _attachEventListeners(): void { - this._register(dom.addDisposableListener(this._parentDomElement, 'mousedown', async (event: MouseEvent) => { + private _attachEventListeners(parentDomElement: HTMLElement, terminalContainer: HTMLElement): void { + this._register(dom.addDisposableListener(parentDomElement, 'mousedown', async (event: MouseEvent) => { if (this._terminalService.terminalInstances.length === 0) { return; } @@ -240,7 +239,7 @@ export class TerminalPanel extends Panel { } } })); - this._register(dom.addDisposableListener(this._parentDomElement, 'mouseup', async (event: MouseEvent) => { + this._register(dom.addDisposableListener(parentDomElement, 'mouseup', async (event: MouseEvent) => { if (this._configurationService.getValue('terminal.integrated.copyOnSelection')) { if (this._terminalService.terminalInstances.length === 0) { return; @@ -254,7 +253,7 @@ export class TerminalPanel extends Panel { } } })); - this._register(dom.addDisposableListener(this._parentDomElement, 'contextmenu', (event: MouseEvent) => { + this._register(dom.addDisposableListener(parentDomElement, 'contextmenu', (event: MouseEvent) => { if (!this._cancelContextMenu) { const standardEvent = new StandardMouseEvent(event); const anchor: { x: number, y: number } = { x: standardEvent.posx, y: standardEvent.posy }; @@ -269,19 +268,19 @@ export class TerminalPanel extends Panel { this._cancelContextMenu = false; })); this._register(dom.addDisposableListener(document, 'keydown', (event: KeyboardEvent) => { - this._terminalContainer.classList.toggle('alt-active', !!event.altKey); + terminalContainer.classList.toggle('alt-active', !!event.altKey); })); this._register(dom.addDisposableListener(document, 'keyup', (event: KeyboardEvent) => { - this._terminalContainer.classList.toggle('alt-active', !!event.altKey); + terminalContainer.classList.toggle('alt-active', !!event.altKey); })); - this._register(dom.addDisposableListener(this._parentDomElement, 'keyup', (event: KeyboardEvent) => { + this._register(dom.addDisposableListener(parentDomElement, 'keyup', (event: KeyboardEvent) => { if (event.keyCode === 27) { // Keep terminal open on escape event.stopPropagation(); } })); - this._register(dom.addDisposableListener(this._parentDomElement, dom.EventType.DROP, async (e: DragEvent) => { - if (e.target === this._parentDomElement || dom.isAncestor(e.target as HTMLElement, this._parentDomElement)) { + this._register(dom.addDisposableListener(parentDomElement, dom.EventType.DROP, async (e: DragEvent) => { + if (e.target === this._parentDomElement || dom.isAncestor(e.target as HTMLElement, parentDomElement)) { if (!e.dataTransfer) { return; } @@ -315,11 +314,13 @@ export class TerminalPanel extends Panel { theme = this.themeService.getTheme(); } - this._findWidget.updateTheme(theme); + if (this._findWidget) { + this._findWidget.updateTheme(theme); + } } private _updateFont(): void { - if (this._terminalService.terminalInstances.length === 0) { + if (this._terminalService.terminalInstances.length === 0 || !this._parentDomElement) { return; } // TODO: Can we support ligatures? diff --git a/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts b/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts index 37dc39b51f6..29e4aa6fbfc 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts @@ -45,7 +45,7 @@ enum ProcessType { export class TerminalProcessManager implements ITerminalProcessManager { public processState: ProcessState = ProcessState.UNINITIALIZED; public ptyProcessReady: Promise; - public shellProcessId: number; + public shellProcessId: number | undefined; public remoteAuthority: string | undefined; public os: platform.OperatingSystem | undefined; public userHome: string | undefined; @@ -54,9 +54,8 @@ export class TerminalProcessManager implements ITerminalProcessManager { private _processType: ProcessType = ProcessType.Process; private _preLaunchInputQueue: string[] = []; private _latency: number = -1; - private _latencyRequest: Promise; private _latencyLastMeasured: number = 0; - private _initialCwd: string; + private _initialCwd: string | undefined; private readonly _onProcessReady = new Emitter(); public get onProcessReady(): Event { return this._onProcessReady.event; } @@ -259,7 +258,7 @@ export class TerminalProcessManager implements ITerminalProcessManager { } public getInitialCwd(): Promise { - return Promise.resolve(this._initialCwd); + return Promise.resolve(this._initialCwd ? this._initialCwd : ''); } public getCwd(): Promise { @@ -275,8 +274,8 @@ export class TerminalProcessManager implements ITerminalProcessManager { return Promise.resolve(0); } if (this._latencyLastMeasured === 0 || this._latencyLastMeasured + LATENCY_MEASURING_INTERVAL < Date.now()) { - this._latencyRequest = this._process.getLatency(); - this._latency = await this._latencyRequest; + const latencyRequest = this._process.getLatency(); + this._latency = await latencyRequest; this._latencyLastMeasured = Date.now(); } return Promise.resolve(this._latency); diff --git a/src/vs/workbench/contrib/terminal/browser/terminalService.ts b/src/vs/workbench/contrib/terminal/browser/terminalService.ts index 9ca71e3d27b..4f584f6d13a 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalService.ts @@ -137,7 +137,7 @@ export class TerminalService extends CommonTerminalService implements ITerminalS public setContainers(panelContainer: HTMLElement, terminalContainer: HTMLElement): void { this._configHelper.panelContainer = panelContainer; this._terminalContainer = terminalContainer; - this._terminalTabs.forEach(tab => tab.attachToElement(this._terminalContainer)); + this._terminalTabs.forEach(tab => tab.attachToElement(terminalContainer)); } public hidePanel(): void { @@ -146,4 +146,4 @@ export class TerminalService extends CommonTerminalService implements ITerminalS this._layoutService.setPanelHidden(true); } } -} \ No newline at end of file +} diff --git a/src/vs/workbench/contrib/terminal/browser/terminalTab.ts b/src/vs/workbench/contrib/terminal/browser/terminalTab.ts index e00296853ff..aefb287ded3 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalTab.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalTab.ts @@ -18,7 +18,7 @@ const TERMINAL_MIN_USEFUL_SIZE = 250; class SplitPaneContainer extends Disposable { private _height: number; private _width: number; - private _splitView: SplitView; + private _splitView!: SplitView; private readonly _splitViewDisposables = this._register(new DisposableStore()); private _children: SplitPane[] = []; @@ -177,7 +177,6 @@ class SplitPane implements IView { public maximumSize: number = Number.MAX_VALUE; public orientation: Orientation | undefined; - protected _size: number; private _onDidChange: Event = Event.None; public get onDidChange(): Event { return this._onDidChange; } @@ -195,15 +194,14 @@ class SplitPane implements IView { public layout(size: number): void { // Only layout when both sizes are known - this._size = size; - if (!this._size || !this.orthogonalSize) { + if (!size || !this.orthogonalSize) { return; } if (this.orientation === Orientation.VERTICAL) { - this.instance.layout({ width: this.orthogonalSize, height: this._size }); + this.instance.layout({ width: this.orthogonalSize, height: size }); } else { - this.instance.layout({ width: this._size, height: this.orthogonalSize }); + this.instance.layout({ width: size, height: this.orthogonalSize }); } } @@ -215,7 +213,7 @@ class SplitPane implements IView { export class TerminalTab extends Disposable implements ITerminalTab { private _terminalInstances: ITerminalInstance[] = []; private _splitPaneContainer: SplitPaneContainer | undefined; - private _tabElement: HTMLElement | null; + private _tabElement: HTMLElement | undefined; private _panelPosition: Position = Position.BOTTOM; private _activeInstanceIndex: number; @@ -230,7 +228,7 @@ export class TerminalTab extends Disposable implements ITerminalTab { constructor( terminalFocusContextKey: IContextKey, configHelper: ITerminalConfigHelper, - private _container: HTMLElement, + private _container: HTMLElement | undefined, shellLaunchConfigOrInstance: IShellLaunchConfig | ITerminalInstance, @ITerminalService private readonly _terminalService: ITerminalService, @IWorkbenchLayoutService private readonly _layoutService: IWorkbenchLayoutService, @@ -259,9 +257,9 @@ export class TerminalTab extends Disposable implements ITerminalTab { public dispose(): void { super.dispose(); - if (this._tabElement) { + if (this._container && this._tabElement) { this._container.removeChild(this._tabElement); - this._tabElement = null; + this._tabElement = undefined; } this._terminalInstances = []; this._onInstancesChanged.fire(); @@ -380,6 +378,9 @@ export class TerminalTab extends Disposable implements ITerminalTab { configHelper: ITerminalConfigHelper, shellLaunchConfig: IShellLaunchConfig ): ITerminalInstance | undefined { + if (!this._container) { + throw new Error('Cannot split terminal that has not been attached'); + } const newTerminalSize = ((this._panelPosition === Position.BOTTOM ? this._container.clientWidth : this._container.clientHeight) / (this._terminalInstances.length + 1)); if (newTerminalSize < TERMINAL_MIN_USEFUL_SIZE) { return undefined; diff --git a/src/vs/workbench/contrib/terminal/browser/terminalWidgetManager.ts b/src/vs/workbench/contrib/terminal/browser/terminalWidgetManager.ts index 8beb67cb689..ce9ed466a98 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalWidgetManager.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalWidgetManager.ts @@ -8,10 +8,10 @@ import { IDisposable, dispose, DisposableStore } from 'vs/base/common/lifecycle' const WIDGET_HEIGHT = 29; export class TerminalWidgetManager implements IDisposable { - private _container: HTMLElement | null; - private _xtermViewport: HTMLElement | null; + private _container: HTMLElement | undefined; + private _xtermViewport: HTMLElement | undefined; - private _messageWidget: MessageWidget; + private _messageWidget: MessageWidget | undefined; private readonly _messageListeners = new DisposableStore(); constructor( @@ -27,9 +27,9 @@ export class TerminalWidgetManager implements IDisposable { public dispose(): void { if (this._container && this._container.parentElement) { this._container.parentElement.removeChild(this._container); - this._container = null; + this._container = undefined; } - this._xtermViewport = null; + this._xtermViewport = undefined; this._messageListeners.dispose(); } @@ -108,4 +108,4 @@ class MessageWidget { this._container.removeChild(this.domNode); } } -} \ No newline at end of file +} diff --git a/src/vs/workbench/contrib/terminal/common/terminal.ts b/src/vs/workbench/contrib/terminal/common/terminal.ts index f6413d9857e..ae7b445e9f8 100644 --- a/src/vs/workbench/contrib/terminal/common/terminal.ts +++ b/src/vs/workbench/contrib/terminal/common/terminal.ts @@ -697,7 +697,7 @@ export interface IBeforeProcessDataEvent { export interface ITerminalProcessManager extends IDisposable { readonly processState: ProcessState; readonly ptyProcessReady: Promise; - readonly shellProcessId: number; + readonly shellProcessId: number | undefined; readonly remoteAuthority: string | undefined; readonly os: OperatingSystem | undefined; readonly userHome: string | undefined; diff --git a/src/vs/workbench/contrib/terminal/common/terminalService.ts b/src/vs/workbench/contrib/terminal/common/terminalService.ts index 8bf6a5bfa5b..e88af8ffc03 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalService.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalService.ts @@ -35,7 +35,7 @@ export abstract class TerminalService implements ITerminalService { protected _isShuttingDown: boolean; protected _terminalFocusContextKey: IContextKey; protected _findWidgetVisible: IContextKey; - protected _terminalContainer: HTMLElement; + protected _terminalContainer: HTMLElement | undefined; protected _terminalTabs: ITerminalTab[] = []; protected _backgroundedTerminalInstances: ITerminalInstance[] = []; protected get _terminalInstances(): ITerminalInstance[] { diff --git a/src/vs/workbench/contrib/terminal/node/terminalProcess.ts b/src/vs/workbench/contrib/terminal/node/terminalProcess.ts index 8b61a562d8d..74d4598d6f5 100644 --- a/src/vs/workbench/contrib/terminal/node/terminalProcess.ts +++ b/src/vs/workbench/contrib/terminal/node/terminalProcess.ts @@ -23,7 +23,7 @@ export class TerminalProcess implements ITerminalChildProcess, IDisposable { private _closeTimeout: any; private _ptyProcess: pty.IPty | undefined; private _currentTitle: string = ''; - private _processStartupComplete: Promise; + private _processStartupComplete: Promise | undefined; private _isDisposed: boolean = false; private _titleInterval: NodeJS.Timer | null = null; private _initialCwd: string; @@ -172,7 +172,7 @@ export class TerminalProcess implements ITerminalChildProcess, IDisposable { private _kill(): void { // Wait to kill to process until the start up code has run. This prevents us from firing a process exit before a // process start. - this._processStartupComplete.then(() => { + this._processStartupComplete!.then(() => { if (this._isDisposed) { return; } diff --git a/src/vs/workbench/contrib/terminal/node/windowsShellHelper.ts b/src/vs/workbench/contrib/terminal/node/windowsShellHelper.ts index ced2d3ee265..030dcd89100 100644 --- a/src/vs/workbench/contrib/terminal/node/windowsShellHelper.ts +++ b/src/vs/workbench/contrib/terminal/node/windowsShellHelper.ts @@ -25,10 +25,10 @@ const SHELL_EXECUTABLES = [ let windowsProcessTree: typeof WindowsProcessTreeType; export class WindowsShellHelper implements IWindowsShellHelper { - private _onCheckShell: Emitter | undefined>; + private _onCheckShell: Emitter | undefined> = new Emitter | undefined>(); private _isDisposed: boolean; - private _currentRequest: Promise | null; - private _newLineFeed: boolean; + private _currentRequest: Promise | undefined; + private _newLineFeed: boolean = false; public constructor( private _rootProcessId: number, @@ -47,7 +47,6 @@ export class WindowsShellHelper implements IWindowsShellHelper { } windowsProcessTree = mod; - this._onCheckShell = new Emitter>(); // The debounce is necessary to prevent multiple processes from spawning when // the enter key or output is spammed Event.debounce(this._onCheckShell.event, (l, e) => e, 150, true)(() => { @@ -65,6 +64,7 @@ export class WindowsShellHelper implements IWindowsShellHelper { this._xterm.onCursorMove(() => { if (this._newLineFeed) { this._onCheckShell.fire(undefined); + this._newLineFeed = false; } }); @@ -127,7 +127,7 @@ export class WindowsShellHelper implements IWindowsShellHelper { this._currentRequest = new Promise(resolve => { windowsProcessTree.getProcessTree(this._rootProcessId, (tree) => { const name = this.traverseTree(tree); - this._currentRequest = null; + this._currentRequest = undefined; resolve(name); }); });