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

@@ -3,9 +3,8 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Terminal, IMarker } from 'xterm';
import { ITerminalCommandTracker } from 'vs/workbench/contrib/terminal/common/terminal';
import { IDisposable } from 'vs/base/common/lifecycle';
import { Terminal, IMarker, ITerminalAddon } from 'xterm';
import { ICommandTracker } from 'vs/workbench/contrib/terminal/common/terminal';
/**
* The minimum size of the prompt in which to assume the line is a command.
@@ -22,15 +21,15 @@ export const enum ScrollPosition {
Middle
}
export class TerminalCommandTracker implements ITerminalCommandTracker, IDisposable {
export class CommandTrackerAddon implements ICommandTracker, ITerminalAddon {
private _currentMarker: IMarker | Boundary = Boundary.Bottom;
private _selectionStart: IMarker | Boundary | null = null;
private _isDisposable: boolean = false;
private _terminal: Terminal | undefined;
constructor(
private _xterm: Terminal
) {
this._xterm.onKey(e => this._onKey(e.key));
public activate(terminal: Terminal): void {
this._terminal = terminal;
terminal.onKey(e => this._onKey(e.key));
}
public dispose(): void {
@@ -48,116 +47,138 @@ export class TerminalCommandTracker implements ITerminalCommandTracker, IDisposa
}
private _onEnter(): void {
if (this._xterm.buffer.cursorX >= MINIMUM_PROMPT_LENGTH) {
this._xterm.addMarker(0);
if (!this._terminal) {
return;
}
if (this._terminal.buffer.cursorX >= MINIMUM_PROMPT_LENGTH) {
this._terminal.addMarker(0);
}
}
public scrollToPreviousCommand(scrollPosition: ScrollPosition = ScrollPosition.Top, retainSelection: boolean = false): void {
if (!this._terminal) {
return;
}
if (!retainSelection) {
this._selectionStart = null;
}
let markerIndex;
if (this._currentMarker === Boundary.Bottom) {
markerIndex = this._xterm.markers.length - 1;
markerIndex = this._terminal.markers.length - 1;
} else if (this._currentMarker === Boundary.Top) {
markerIndex = -1;
} else if (this._isDisposable) {
markerIndex = this._findPreviousCommand();
markerIndex = this._findPreviousCommand(this._terminal);
this._currentMarker.dispose();
this._isDisposable = false;
} else {
markerIndex = this._xterm.markers.indexOf(this._currentMarker) - 1;
markerIndex = this._terminal.markers.indexOf(this._currentMarker) - 1;
}
if (markerIndex < 0) {
this._currentMarker = Boundary.Top;
this._xterm.scrollToTop();
this._terminal.scrollToTop();
return;
}
this._currentMarker = this._xterm.markers[markerIndex];
this._currentMarker = this._terminal.markers[markerIndex];
this._scrollToMarker(this._currentMarker, scrollPosition);
}
public scrollToNextCommand(scrollPosition: ScrollPosition = ScrollPosition.Top, retainSelection: boolean = false): void {
if (!this._terminal) {
return;
}
if (!retainSelection) {
this._selectionStart = null;
}
let markerIndex;
if (this._currentMarker === Boundary.Bottom) {
markerIndex = this._xterm.markers.length;
markerIndex = this._terminal.markers.length;
} else if (this._currentMarker === Boundary.Top) {
markerIndex = 0;
} else if (this._isDisposable) {
markerIndex = this._findNextCommand();
markerIndex = this._findNextCommand(this._terminal);
this._currentMarker.dispose();
this._isDisposable = false;
} else {
markerIndex = this._xterm.markers.indexOf(this._currentMarker) + 1;
markerIndex = this._terminal.markers.indexOf(this._currentMarker) + 1;
}
if (markerIndex >= this._xterm.markers.length) {
if (markerIndex >= this._terminal.markers.length) {
this._currentMarker = Boundary.Bottom;
this._xterm.scrollToBottom();
this._terminal.scrollToBottom();
return;
}
this._currentMarker = this._xterm.markers[markerIndex];
this._currentMarker = this._terminal.markers[markerIndex];
this._scrollToMarker(this._currentMarker, scrollPosition);
}
private _scrollToMarker(marker: IMarker, position: ScrollPosition): void {
if (!this._terminal) {
return;
}
let line = marker.line;
if (position === ScrollPosition.Middle) {
line = Math.max(line - Math.floor(this._xterm.rows / 2), 0);
line = Math.max(line - Math.floor(this._terminal.rows / 2), 0);
}
this._xterm.scrollToLine(line);
this._terminal.scrollToLine(line);
}
public selectToPreviousCommand(): void {
if (!this._terminal) {
return;
}
if (this._selectionStart === null) {
this._selectionStart = this._currentMarker;
}
this.scrollToPreviousCommand(ScrollPosition.Middle, true);
this._selectLines(this._currentMarker, this._selectionStart);
this._selectLines(this._terminal, this._currentMarker, this._selectionStart);
}
public selectToNextCommand(): void {
if (!this._terminal) {
return;
}
if (this._selectionStart === null) {
this._selectionStart = this._currentMarker;
}
this.scrollToNextCommand(ScrollPosition.Middle, true);
this._selectLines(this._currentMarker, this._selectionStart);
this._selectLines(this._terminal, this._currentMarker, this._selectionStart);
}
public selectToPreviousLine(): void {
if (!this._terminal) {
return;
}
if (this._selectionStart === null) {
this._selectionStart = this._currentMarker;
}
this.scrollToPreviousLine(ScrollPosition.Middle, true);
this._selectLines(this._currentMarker, this._selectionStart);
this.scrollToPreviousLine(this._terminal, ScrollPosition.Middle, true);
this._selectLines(this._terminal, this._currentMarker, this._selectionStart);
}
public selectToNextLine(): void {
if (!this._terminal) {
return;
}
if (this._selectionStart === null) {
this._selectionStart = this._currentMarker;
}
this.scrollToNextLine(ScrollPosition.Middle, true);
this._selectLines(this._currentMarker, this._selectionStart);
this.scrollToNextLine(this._terminal, ScrollPosition.Middle, true);
this._selectLines(this._terminal, this._currentMarker, this._selectionStart);
}
private _selectLines(start: IMarker | Boundary, end: IMarker | Boundary | null): void {
private _selectLines(xterm: Terminal, start: IMarker | Boundary, end: IMarker | Boundary | null): void {
if (end === null) {
end = Boundary.Bottom;
}
let startLine = this._getLine(start);
let endLine = this._getLine(end);
let startLine = this._getLine(xterm, start);
let endLine = this._getLine(xterm, end);
if (startLine > endLine) {
const temp = startLine;
@@ -169,13 +190,13 @@ export class TerminalCommandTracker implements ITerminalCommandTracker, IDisposa
// command in the selection for the current command
endLine -= 1;
this._xterm.selectLines(startLine, endLine);
xterm.selectLines(startLine, endLine);
}
private _getLine(marker: IMarker | Boundary): number {
private _getLine(xterm: Terminal, marker: IMarker | Boundary): number {
// Use the _second last_ row as the last row is likely the prompt
if (marker === Boundary.Bottom) {
return this._xterm.buffer.baseY + this._xterm.rows - 1;
return xterm.buffer.baseY + xterm.rows - 1;
}
if (marker === Boundary.Top) {
@@ -185,74 +206,74 @@ export class TerminalCommandTracker implements ITerminalCommandTracker, IDisposa
return marker.line;
}
public scrollToPreviousLine(scrollPosition: ScrollPosition = ScrollPosition.Top, retainSelection: boolean = false): void {
public scrollToPreviousLine(xterm: Terminal, scrollPosition: ScrollPosition = ScrollPosition.Top, retainSelection: boolean = false): void {
if (!retainSelection) {
this._selectionStart = null;
}
if (this._currentMarker === Boundary.Top) {
this._xterm.scrollToTop();
xterm.scrollToTop();
return;
}
if (this._currentMarker === Boundary.Bottom) {
this._currentMarker = this._xterm.addMarker(this._getOffset() - 1);
this._currentMarker = xterm.addMarker(this._getOffset(xterm) - 1);
} else {
const offset = this._getOffset();
const offset = this._getOffset(xterm);
if (this._isDisposable) {
this._currentMarker.dispose();
}
this._currentMarker = this._xterm.addMarker(offset - 1);
this._currentMarker = xterm.addMarker(offset - 1);
}
this._isDisposable = true;
this._scrollToMarker(this._currentMarker, scrollPosition);
}
public scrollToNextLine(scrollPosition: ScrollPosition = ScrollPosition.Top, retainSelection: boolean = false): void {
public scrollToNextLine(xterm: Terminal, scrollPosition: ScrollPosition = ScrollPosition.Top, retainSelection: boolean = false): void {
if (!retainSelection) {
this._selectionStart = null;
}
if (this._currentMarker === Boundary.Bottom) {
this._xterm.scrollToBottom();
xterm.scrollToBottom();
return;
}
if (this._currentMarker === Boundary.Top) {
this._currentMarker = this._xterm.addMarker(this._getOffset() + 1);
this._currentMarker = xterm.addMarker(this._getOffset(xterm) + 1);
} else {
const offset = this._getOffset();
const offset = this._getOffset(xterm);
if (this._isDisposable) {
this._currentMarker.dispose();
}
this._currentMarker = this._xterm.addMarker(offset + 1);
this._currentMarker = xterm.addMarker(offset + 1);
}
this._isDisposable = true;
this._scrollToMarker(this._currentMarker, scrollPosition);
}
private _getOffset(): number {
private _getOffset(xterm: Terminal): number {
if (this._currentMarker === Boundary.Bottom) {
return 0;
} else if (this._currentMarker === Boundary.Top) {
return 0 - (this._xterm.buffer.baseY + this._xterm.buffer.cursorY);
return 0 - (xterm.buffer.baseY + xterm.buffer.cursorY);
} else {
let offset = this._getLine(this._currentMarker);
offset -= this._xterm.buffer.baseY + this._xterm.buffer.cursorY;
let offset = this._getLine(xterm, this._currentMarker);
offset -= xterm.buffer.baseY + xterm.buffer.cursorY;
return offset;
}
}
private _findPreviousCommand(): number {
private _findPreviousCommand(xterm: Terminal): number {
if (this._currentMarker === Boundary.Top) {
return 0;
} else if (this._currentMarker === Boundary.Bottom) {
return this._xterm.markers.length - 1;
return xterm.markers.length - 1;
}
let i;
for (i = this._xterm.markers.length - 1; i >= 0; i--) {
if (this._xterm.markers[i].line < this._currentMarker.line) {
for (i = xterm.markers.length - 1; i >= 0; i--) {
if (xterm.markers[i].line < this._currentMarker.line) {
return i;
}
}
@@ -260,20 +281,20 @@ export class TerminalCommandTracker implements ITerminalCommandTracker, IDisposa
return -1;
}
private _findNextCommand(): number {
private _findNextCommand(xterm: Terminal): number {
if (this._currentMarker === Boundary.Top) {
return 0;
} else if (this._currentMarker === Boundary.Bottom) {
return this._xterm.markers.length - 1;
return xterm.markers.length - 1;
}
let i;
for (i = 0; i < this._xterm.markers.length; i++) {
if (this._xterm.markers[i].line > this._currentMarker.line) {
for (i = 0; i < xterm.markers.length; i++) {
if (xterm.markers[i].line > this._currentMarker.line) {
return i;
}
}
return this._xterm.markers.length;
return xterm.markers.length;
}
}

View File

@@ -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<boolean>
@@ -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 = <HTMLElement | null>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 = <HTMLElement | null>document.activeElement.nextElementSibling;
@@ -103,4 +114,4 @@ export class NavigationModeAddon implements INavigationMode, ITerminalAddon {
});
this._navigationModeContextKey.set(true);
}
}
}

View File

@@ -71,6 +71,21 @@ configurationRegistry.registerConfiguration({
title: nls.localize('terminalIntegratedConfigurationTitle', "Integrated Terminal"),
type: 'object',
properties: {
'terminal.integrated.automationShell.linux': {
markdownDescription: nls.localize('terminal.integrated.automationShell.linux', "A path that when set will override {0} and ignore {1} and {2} values for automation-related terminal usage like tasks and debug.", '`terminal.integrated.shell.linux`', '`shellArgs`', '`env`'),
type: ['string', 'null'],
default: null
},
'terminal.integrated.automationShell.osx': {
markdownDescription: nls.localize('terminal.integrated.automationShell.osx', "A path that when set will override {0} and ignore {1} and {2} values for automation-related terminal usage like tasks and debug.", '`terminal.integrated.shell.osx`', '`shellArgs`', '`env`'),
type: ['string', 'null'],
default: null
},
'terminal.integrated.automationShell.windows': {
markdownDescription: nls.localize('terminal.integrated.automationShell.windows', "A path that when set will override {0} and ignore {1} and {2} values for automation-related terminal usage like tasks and debug.", '`terminal.integrated.shell.windows`', '`shellArgs`', '`env`'),
type: ['string', 'null'],
default: null
},
'terminal.integrated.shellArgs.linux': {
markdownDescription: nls.localize('terminal.integrated.shellArgs.linux', "The command line arguments to use when on the Linux terminal. [Read more about configuring the shell](https://code.visualstudio.com/docs/editor/integrated-terminal#_configuration)."),
type: 'array',
@@ -450,11 +465,11 @@ actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(ResizePaneDownTe
actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(ScrollToPreviousCommandAction, ScrollToPreviousCommandAction.ID, ScrollToPreviousCommandAction.LABEL, {
primary: 0,
mac: { primary: KeyMod.CtrlCmd | KeyCode.UpArrow }
}, KEYBINDING_CONTEXT_TERMINAL_FOCUS), 'Terminal: Scroll To Previous Command', category);
}, ContextKeyExpr.and(KEYBINDING_CONTEXT_TERMINAL_FOCUS, CONTEXT_ACCESSIBILITY_MODE_ENABLED.negate())), 'Terminal: Scroll To Previous Command', category);
actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(ScrollToNextCommandAction, ScrollToNextCommandAction.ID, ScrollToNextCommandAction.LABEL, {
primary: 0,
mac: { primary: KeyMod.CtrlCmd | KeyCode.DownArrow }
}, KEYBINDING_CONTEXT_TERMINAL_FOCUS), 'Terminal: Scroll To Next Command', category);
}, ContextKeyExpr.and(KEYBINDING_CONTEXT_TERMINAL_FOCUS, CONTEXT_ACCESSIBILITY_MODE_ENABLED.negate())), 'Terminal: Scroll To Next Command', category);
actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(SelectToPreviousCommandAction, SelectToPreviousCommandAction.ID, SelectToPreviousCommandAction.LABEL, {
primary: 0,
mac: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.UpArrow }

View File

@@ -30,10 +30,10 @@ export interface ITerminalInstanceService {
createWindowsShellHelper(shellProcessId: number, instance: ITerminalInstance, xterm: XTermTerminal): IWindowsShellHelper;
createTerminalProcess(shellLaunchConfig: IShellLaunchConfig, cwd: string, cols: number, rows: number, env: IProcessEnvironment, windowsEnableConpty: boolean): ITerminalChildProcess;
getDefaultShellAndArgs(platformOverride?: Platform): Promise<{ shell: string, args: string[] | string | undefined }>;
getDefaultShellAndArgs(useAutomationShell: boolean, platformOverride?: Platform): Promise<{ shell: string, args: string[] | string | undefined }>;
getMainProcessParentEnv(): Promise<IProcessEnvironment>;
}
export interface IBrowserTerminalConfigHelper extends ITerminalConfigHelper {
panelContainer: HTMLElement;
panelContainer: HTMLElement | undefined;
}

View File

@@ -1168,7 +1168,7 @@ export class ScrollToPreviousCommandAction extends Action {
public run(): Promise<any> {
const instance = this.terminalService.getActiveInstance();
if (instance) {
if (instance && instance.commandTracker) {
instance.commandTracker.scrollToPreviousCommand();
instance.focus();
}
@@ -1189,7 +1189,7 @@ export class ScrollToNextCommandAction extends Action {
public run(): Promise<any> {
const instance = this.terminalService.getActiveInstance();
if (instance) {
if (instance && instance.commandTracker) {
instance.commandTracker.scrollToNextCommand();
instance.focus();
}
@@ -1210,7 +1210,7 @@ export class SelectToPreviousCommandAction extends Action {
public run(): Promise<any> {
const instance = this.terminalService.getActiveInstance();
if (instance) {
if (instance && instance.commandTracker) {
instance.commandTracker.selectToPreviousCommand();
instance.focus();
}
@@ -1231,7 +1231,7 @@ export class SelectToNextCommandAction extends Action {
public run(): Promise<any> {
const instance = this.terminalService.getActiveInstance();
if (instance) {
if (instance && instance.commandTracker) {
instance.commandTracker.selectToNextCommand();
instance.focus();
}
@@ -1252,7 +1252,7 @@ export class SelectToPreviousLineAction extends Action {
public run(): Promise<any> {
const instance = this.terminalService.getActiveInstance();
if (instance) {
if (instance && instance.commandTracker) {
instance.commandTracker.selectToPreviousLine();
instance.focus();
}
@@ -1273,7 +1273,7 @@ export class SelectToNextLineAction extends Action {
public run(): Promise<any> {
const instance = this.terminalService.getActiveInstance();
if (instance) {
if (instance && instance.commandTracker) {
instance.commandTracker.selectToNextLine();
instance.focus();
}

View File

@@ -26,11 +26,11 @@ const MAXIMUM_FONT_SIZE = 25;
* specific test cases can be written.
*/
export class TerminalConfigHelper implements IBrowserTerminalConfigHelper {
public panelContainer: HTMLElement;
public panelContainer: HTMLElement | undefined;
private _charMeasureElement: HTMLElement;
private _lastFontMeasurement: ITerminalFont;
public config: ITerminalConfiguration;
private _charMeasureElement: HTMLElement | undefined;
private _lastFontMeasurement: ITerminalFont | undefined;
public config!: ITerminalConfiguration;
private readonly _onWorkspacePermissionsChanged = new Emitter<boolean>();
public get onWorkspacePermissionsChanged(): Event<boolean> { return this._onWorkspacePermissionsChanged.event; }
@@ -55,49 +55,55 @@ export class TerminalConfigHelper implements IBrowserTerminalConfigHelper {
}
public configFontIsMonospace(): boolean {
this._createCharMeasureElementIfNecessary();
const fontSize = 15;
const fontFamily = this.config.fontFamily || this._configurationService.getValue<IEditorOptions>('editor').fontFamily || EDITOR_FONT_DEFAULTS.fontFamily;
const i_rect = this._getBoundingRectFor('i', fontFamily, fontSize);
const w_rect = this._getBoundingRectFor('w', fontFamily, fontSize);
const invalidBounds = !i_rect.width || !w_rect.width;
if (invalidBounds) {
// There is no reason to believe the font is not Monospace.
// Check for invalid bounds, there is no reason to believe the font is not monospace
if (!i_rect || !w_rect || !i_rect.width || !w_rect.width) {
return true;
}
return i_rect.width === w_rect.width;
}
private _createCharMeasureElementIfNecessary() {
private _createCharMeasureElementIfNecessary(): HTMLElement {
if (!this.panelContainer) {
throw new Error('Cannot measure element when terminal is not attached');
}
// Create charMeasureElement if it hasn't been created or if it was orphaned by its parent
if (!this._charMeasureElement || !this._charMeasureElement.parentElement) {
this._charMeasureElement = document.createElement('div');
this.panelContainer.appendChild(this._charMeasureElement);
}
return this._charMeasureElement;
}
private _getBoundingRectFor(char: string, fontFamily: string, fontSize: number): ClientRect | DOMRect {
const style = this._charMeasureElement.style;
private _getBoundingRectFor(char: string, fontFamily: string, fontSize: number): ClientRect | DOMRect | undefined {
let charMeasureElement: HTMLElement;
try {
charMeasureElement = this._createCharMeasureElementIfNecessary();
} catch {
return undefined;
}
const style = charMeasureElement.style;
style.display = 'inline-block';
style.fontFamily = fontFamily;
style.fontSize = fontSize + 'px';
style.lineHeight = 'normal';
this._charMeasureElement.innerText = char;
const rect = this._charMeasureElement.getBoundingClientRect();
charMeasureElement.innerText = char;
const rect = charMeasureElement.getBoundingClientRect();
style.display = 'none';
return rect;
}
private _measureFont(fontFamily: string, fontSize: number, letterSpacing: number, lineHeight: number): ITerminalFont {
this._createCharMeasureElementIfNecessary();
const rect = this._getBoundingRectFor('X', fontFamily, fontSize);
// Bounding client rect was invalid, use last font measurement if available.
if (this._lastFontMeasurement && !rect.width && !rect.height) {
if (this._lastFontMeasurement && (!rect || !rect.width || !rect.height)) {
return this._lastFontMeasurement;
}
@@ -106,8 +112,8 @@ export class TerminalConfigHelper implements IBrowserTerminalConfigHelper {
fontSize,
letterSpacing,
lineHeight,
charWidth: rect.width,
charHeight: Math.ceil(rect.height)
charWidth: rect && rect.width ? rect.width : 0,
charHeight: rect && rect.height ? Math.ceil(rect.height) : 0
};
return this._lastFontMeasurement;
}

View File

@@ -3,7 +3,7 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { SimpleFindWidget } from 'vs/editor/contrib/find/simpleFindWidget';
import { SimpleFindWidget } from 'vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget';
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
import { ITerminalService, KEYBINDING_CONTEXT_TERMINAL_FIND_WIDGET_INPUT_FOCUSED, KEYBINDING_CONTEXT_TERMINAL_FIND_WIDGET_FOCUSED } from 'vs/workbench/contrib/terminal/common/terminal';
import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey';
@@ -78,4 +78,14 @@ export class TerminalFindWidget extends SimpleFindWidget {
protected onFindInputFocusTrackerBlur() {
this._findInputFocused.reset();
}
public findFirst() {
const instance = this._terminalService.getActiveInstance();
if (instance) {
if (instance.hasSelection()) {
instance.clearSelection();
}
instance.findPrevious(this.inputValue, { regex: this._getRegexValue(), wholeWord: this._getWholeWordValue(), caseSensitive: this._getCaseSensitiveValue() });
}
}
}

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();
}
}

View File

@@ -10,6 +10,7 @@ import { WebLinksAddon as XTermWebLinksAddon } from 'xterm-addon-web-links';
import { SearchAddon as XTermSearchAddon } from 'xterm-addon-search';
import { IProcessEnvironment } from 'vs/base/common/platform';
import { Emitter, Event } from 'vs/base/common/event';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
let Terminal: typeof XTermTerminal;
let WebLinksAddon: typeof XTermWebLinksAddon;
@@ -52,11 +53,16 @@ export class TerminalInstanceService implements ITerminalInstanceService {
throw new Error('Not implemented');
}
public getDefaultShellAndArgs(): Promise<{ shell: string, args: string[] | string | undefined }> {
return new Promise(r => this._onRequestDefaultShellAndArgs.fire((shell, args) => r({ shell, args })));
public getDefaultShellAndArgs(useAutomationShell: boolean, ): Promise<{ shell: string, args: string[] | string | undefined }> {
return new Promise(r => this._onRequestDefaultShellAndArgs.fire({
useAutomationShell,
callback: (shell, args) => r({ shell, args })
}));
}
public async getMainProcessParentEnv(): Promise<IProcessEnvironment> {
return {};
}
}
}
registerSingleton(ITerminalInstanceService, TerminalInstanceService, true);

View File

@@ -5,15 +5,15 @@
import * as nls from 'vs/nls';
import { URI } from 'vs/base/common/uri';
import { dispose, IDisposable, DisposableStore } from 'vs/base/common/lifecycle';
import { DisposableStore } from 'vs/base/common/lifecycle';
import { IOpenerService } from 'vs/platform/opener/common/opener';
import { TerminalWidgetManager } from 'vs/workbench/contrib/terminal/browser/terminalWidgetManager';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { ITerminalService, ITerminalProcessManager } from 'vs/workbench/contrib/terminal/common/terminal';
import { ITerminalProcessManager, ITerminalConfigHelper } from 'vs/workbench/contrib/terminal/common/terminal';
import { ITextEditorSelection } from 'vs/platform/editor/common/editor';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { IFileService } from 'vs/platform/files/common/files';
import { ILinkMatcherOptions } from 'xterm';
import { Terminal, ILinkMatcherOptions } from 'xterm';
import { REMOTE_HOST_SCHEME } from 'vs/platform/remote/common/remoteHosts';
import { posix, win32 } from 'vs/base/common/path';
import { ITerminalInstanceService } from 'vs/workbench/contrib/terminal/browser/terminal';
@@ -67,21 +67,20 @@ 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;
private readonly _leaveCallback: () => void;
constructor(
private _xterm: any,
private _xterm: Terminal,
private readonly _processManager: ITerminalProcessManager | undefined,
private readonly _configHelper: ITerminalConfigHelper,
@IOpenerService private readonly _openerService: IOpenerService,
@IEditorService private readonly _editorService: IEditorService,
@IConfigurationService private readonly _configurationService: IConfigurationService,
@ITerminalService private readonly _terminalService: ITerminalService,
@ITerminalInstanceService private readonly _terminalInstanceService: ITerminalInstanceService,
@IFileService private readonly _fileService: IFileService
) {
@@ -94,7 +93,7 @@ export class TerminalLinkHandler {
if (!this._widgetManager) {
return;
}
if (this._terminalService && this._terminalService.configHelper.config.rendererType === 'dom') {
if (this._configHelper.config.rendererType === 'dom') {
const target = (e.target as HTMLElement);
this._widgetManager.showMessage(target.offsetLeft, target.offsetTop, this._getLinkHoverString());
} else {
@@ -183,10 +182,7 @@ export class TerminalLinkHandler {
}
public dispose(): void {
this._xterm = null;
this._hoverDisposables.dispose();
this._mouseMoveDisposable = dispose(this._mouseMoveDisposable);
}
private _wrapLinkHandler(handler: (uri: string) => boolean | void): XtermLinkMatcherHandler {
@@ -195,9 +191,6 @@ export class TerminalLinkHandler {
event.preventDefault();
// Require correct modifier on click
if (!this._isLinkActivationModifierDown(event)) {
// If the modifier is not pressed, the terminal should be
// focused if it's not already
this._terminalService.getActiveInstance()!.focus(true);
return false;
}
return handler(uri);

View File

@@ -6,6 +6,7 @@
import { IOpenFileRequest } from 'vs/platform/windows/common/windows';
import { ITerminalNativeService, LinuxDistro } from 'vs/workbench/contrib/terminal/common/terminal';
import { Emitter, Event } from 'vs/base/common/event';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
export class TerminalNativeService implements ITerminalNativeService {
public _serviceBrand: any;
@@ -30,4 +31,6 @@ export class TerminalNativeService implements ITerminalNativeService {
public getWindowsBuildNumber(): number {
throw new Error('Not implemented');
}
}
}
registerSingleton(ITerminalNativeService, TerminalNativeService, true);

View File

@@ -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,21 +239,7 @@ export class TerminalPanel extends Panel {
}
}
}));
this._register(dom.addDisposableListener(this._parentDomElement, 'mouseup', async (event: MouseEvent) => {
if (this._configurationService.getValue('terminal.integrated.copyOnSelection')) {
if (this._terminalService.terminalInstances.length === 0) {
return;
}
if (event.which === 1) {
const terminal = this._terminalService.getActiveInstance();
if (terminal && terminal.hasSelection()) {
await terminal.copySelection();
}
}
}
}));
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 +254,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 +300,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?

View File

@@ -20,6 +20,7 @@ import { IProductService } from 'vs/platform/product/common/product';
import { ITerminalInstanceService } from 'vs/workbench/contrib/terminal/browser/terminal';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService';
import { Disposable } from 'vs/base/common/lifecycle';
/** The amount of time to consider terminal errors to be related to the launch */
const LAUNCHING_DURATION = 500;
@@ -42,10 +43,10 @@ enum ProcessType {
* - Pty Process: The pseudoterminal master process (or the winpty agent process)
* - Shell Process: The pseudoterminal slave process (ie. the shell)
*/
export class TerminalProcessManager implements ITerminalProcessManager {
export class TerminalProcessManager extends Disposable implements ITerminalProcessManager {
public processState: ProcessState = ProcessState.UNINITIALIZED;
public ptyProcessReady: Promise<void>;
public shellProcessId: number;
public shellProcessId: number | undefined;
public remoteAuthority: string | undefined;
public os: platform.OperatingSystem | undefined;
public userHome: string | undefined;
@@ -54,23 +55,22 @@ export class TerminalProcessManager implements ITerminalProcessManager {
private _processType: ProcessType = ProcessType.Process;
private _preLaunchInputQueue: string[] = [];
private _latency: number = -1;
private _latencyRequest: Promise<number>;
private _latencyLastMeasured: number = 0;
private _initialCwd: string;
private _initialCwd: string | undefined;
private readonly _onProcessReady = new Emitter<void>();
private readonly _onProcessReady = this._register(new Emitter<void>());
public get onProcessReady(): Event<void> { return this._onProcessReady.event; }
private readonly _onBeforeProcessData = new Emitter<IBeforeProcessDataEvent>();
private readonly _onBeforeProcessData = this._register(new Emitter<IBeforeProcessDataEvent>());
public get onBeforeProcessData(): Event<IBeforeProcessDataEvent> { return this._onBeforeProcessData.event; }
private readonly _onProcessData = new Emitter<string>();
private readonly _onProcessData = this._register(new Emitter<string>());
public get onProcessData(): Event<string> { return this._onProcessData.event; }
private readonly _onProcessTitle = new Emitter<string>();
private readonly _onProcessTitle = this._register(new Emitter<string>());
public get onProcessTitle(): Event<string> { return this._onProcessTitle.event; }
private readonly _onProcessExit = new Emitter<number>();
private readonly _onProcessExit = this._register(new Emitter<number>());
public get onProcessExit(): Event<number> { return this._onProcessExit.event; }
private readonly _onProcessOverrideDimensions = new Emitter<ITerminalDimensions | undefined>();
private readonly _onProcessOverrideDimensions = this._register(new Emitter<ITerminalDimensions | undefined>());
public get onProcessOverrideDimensions(): Event<ITerminalDimensions | undefined> { return this._onProcessOverrideDimensions.event; }
private readonly _onProcessOverrideShellLaunchConfig = new Emitter<IShellLaunchConfig>();
private readonly _onProcessOverrideShellLaunchConfig = this._register(new Emitter<IShellLaunchConfig>());
public get onProcessResolvedShellLaunchConfig(): Event<IShellLaunchConfig> { return this._onProcessOverrideShellLaunchConfig.event; }
constructor(
@@ -87,6 +87,7 @@ export class TerminalProcessManager implements ITerminalProcessManager {
@ITerminalInstanceService private readonly _terminalInstanceService: ITerminalInstanceService,
@IRemoteAgentService private readonly _remoteAgentService: IRemoteAgentService
) {
super();
this.ptyProcessReady = new Promise<void>(c => {
this.onProcessReady(() => {
this._logService.debug(`Terminal process ready (shellProcessId: ${this.shellProcessId})`);
@@ -105,6 +106,7 @@ export class TerminalProcessManager implements ITerminalProcessManager {
this._process.shutdown(immediate);
this._process = null;
}
super.dispose();
}
public async createProcess(
@@ -193,7 +195,7 @@ export class TerminalProcessManager implements ITerminalProcessManager {
const platformKey = platform.isWindows ? 'windows' : (platform.isMacintosh ? 'osx' : 'linux');
const lastActiveWorkspace = activeWorkspaceRootUri ? this._workspaceContextService.getWorkspaceFolder(activeWorkspaceRootUri) : null;
if (!shellLaunchConfig.executable) {
const defaultConfig = await this._terminalInstanceService.getDefaultShellAndArgs();
const defaultConfig = await this._terminalInstanceService.getDefaultShellAndArgs(false);
shellLaunchConfig.executable = defaultConfig.shell;
shellLaunchConfig.args = defaultConfig.args;
} else {
@@ -259,7 +261,7 @@ export class TerminalProcessManager implements ITerminalProcessManager {
}
public getInitialCwd(): Promise<string> {
return Promise.resolve(this._initialCwd);
return Promise.resolve(this._initialCwd ? this._initialCwd : '');
}
public getCwd(): Promise<string> {
@@ -275,8 +277,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);

View File

@@ -5,7 +5,7 @@
import { ITerminalService, TERMINAL_PANEL_ID, ITerminalInstance, IShellLaunchConfig, ITerminalConfigHelper, ITerminalNativeService } from 'vs/workbench/contrib/terminal/common/terminal';
import { TerminalService as CommonTerminalService } from 'vs/workbench/contrib/terminal/common/terminalService';
import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IPanelService } from 'vs/workbench/services/panel/common/panelService';
import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService';
import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle';
@@ -49,18 +49,15 @@ export class TerminalService extends CommonTerminalService implements ITerminalS
this._configHelper = this._instantiationService.createInstance(TerminalConfigHelper, this.terminalNativeService.linuxDistro);
}
public createInstance(terminalFocusContextKey: IContextKey<boolean>, configHelper: ITerminalConfigHelper, container: HTMLElement | undefined, shellLaunchConfig: IShellLaunchConfig): ITerminalInstance {
const instance = this._instantiationService.createInstance(TerminalInstance, terminalFocusContextKey, configHelper, container, shellLaunchConfig);
public createInstance(container: HTMLElement | undefined, shellLaunchConfig: IShellLaunchConfig): ITerminalInstance {
const instance = this._instantiationService.createInstance(TerminalInstance, this._terminalFocusContextKey, this._configHelper, container, shellLaunchConfig);
this._onInstanceCreated.fire(instance);
return instance;
}
public createTerminal(shell: IShellLaunchConfig = {}): ITerminalInstance {
if (shell.hideFromUser) {
const instance = this.createInstance(this._terminalFocusContextKey,
this.configHelper,
undefined,
shell);
const instance = this.createInstance(undefined, shell);
this._backgroundedTerminalInstances.push(instance);
this._initInstanceListeners(instance);
return instance;
@@ -137,7 +134,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 {

View File

@@ -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<number | undefined> = Event.None;
public get onDidChange(): Event<number | undefined> { 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,22 +213,22 @@ 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;
public get terminalInstances(): ITerminalInstance[] { return this._terminalInstances; }
private readonly _onDisposed: Emitter<ITerminalTab> = new Emitter<ITerminalTab>();
private readonly _onDisposed: Emitter<ITerminalTab> = this._register(new Emitter<ITerminalTab>());
public readonly onDisposed: Event<ITerminalTab> = this._onDisposed.event;
private readonly _onInstancesChanged: Emitter<void> = new Emitter<void>();
private readonly _onInstancesChanged: Emitter<void> = this._register(new Emitter<void>());
public readonly onInstancesChanged: Event<void> = this._onInstancesChanged.event;
constructor(
terminalFocusContextKey: IContextKey<boolean>,
configHelper: ITerminalConfigHelper,
private _container: HTMLElement,
private _container: HTMLElement | undefined,
shellLaunchConfigOrInstance: IShellLaunchConfig | ITerminalInstance,
@ITerminalService private readonly _terminalService: ITerminalService,
@IWorkbenchLayoutService private readonly _layoutService: IWorkbenchLayoutService,
@@ -242,11 +240,7 @@ export class TerminalTab extends Disposable implements ITerminalTab {
if ('id' in shellLaunchConfigOrInstance) {
instance = shellLaunchConfigOrInstance;
} else {
instance = this._terminalService.createInstance(
terminalFocusContextKey,
configHelper,
undefined,
shellLaunchConfigOrInstance);
instance = this._terminalService.createInstance(undefined, shellLaunchConfigOrInstance);
}
this._terminalInstances.push(instance);
this._initInstanceListeners(instance);
@@ -259,9 +253,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,15 +374,14 @@ 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;
}
const instance = this._terminalService.createInstance(
terminalFocusContextKey,
configHelper,
undefined,
shellLaunchConfig);
const instance = this._terminalService.createInstance(undefined, shellLaunchConfig);
this._terminalInstances.splice(this._activeInstanceIndex + 1, 0, instance);
this._initInstanceListeners(instance);
this._setActiveInstance(instance);

View File

@@ -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);
}
}
}
}

View File

@@ -76,6 +76,11 @@ export interface ITerminalConfiguration {
osx: string | null;
windows: string | null;
};
automationShell: {
linux: string | null;
osx: string | null;
windows: string | null;
};
shellArgs: {
linux: string[];
osx: string[];
@@ -187,11 +192,6 @@ export interface IShellLaunchConfig {
*/
initialText?: string;
/**
* @deprecated use `isExtensionTerminal`
*/
isRendererOnly?: boolean;
/**
* Whether an extension is controlling the terminal via a `vscode.Pseudoterminal`.
*/
@@ -244,16 +244,10 @@ export interface ITerminalService {
*/
createTerminal(shell?: IShellLaunchConfig): ITerminalInstance;
/**
* Creates a terminal renderer.
* @param name The name of the terminal.
*/
createTerminalRenderer(name: string): ITerminalInstance;
/**
* Creates a raw terminal instance, this should not be used outside of the terminal part.
*/
createInstance(terminalFocusContextKey: IContextKey<boolean>, configHelper: ITerminalConfigHelper, container: HTMLElement | undefined, shellLaunchConfig: IShellLaunchConfig): ITerminalInstance;
createInstance(container: HTMLElement | undefined, shellLaunchConfig: IShellLaunchConfig): ITerminalInstance;
getInstanceFromId(terminalId: number): ITerminalInstance | undefined;
getInstanceFromIndex(terminalIndex: number): ITerminalInstance;
getTabLabels(): string[];
@@ -422,13 +416,6 @@ export interface ITerminalInstance {
*/
onData: Event<string>;
/**
* Attach a listener to the "renderer" input event, this event fires for terminal renderers on
* keystrokes and when the Terminal.sendText extension API is used.
* @param listener The listener function.
*/
onRendererInput: Event<string>;
/**
* Attach a listener to listen for new lines added to this terminal instance.
*
@@ -483,7 +470,7 @@ export interface ITerminalInstance {
* An object that tracks when commands are run and enables navigating and selecting between
* them.
*/
readonly commandTracker: ITerminalCommandTracker;
readonly commandTracker: ICommandTracker | undefined;
readonly navigationMode: INavigationMode | undefined;
@@ -497,14 +484,6 @@ export interface ITerminalInstance {
*/
dispose(immediate?: boolean): void;
/**
* Indicates that a consumer of a renderer only terminal is finished with it.
*
* @param exitCode The exit code of the terminal. Zero indicates success, non-zero indicates
* failure.
*/
rendererExit(exitCode: number): void;
/**
* Forces the terminal to redraw its viewport.
*/
@@ -671,7 +650,7 @@ export interface ITerminalInstance {
getCwd(): Promise<string>;
}
export interface ITerminalCommandTracker {
export interface ICommandTracker {
scrollToPreviousCommand(): void;
scrollToNextCommand(): void;
selectToPreviousCommand(): void;
@@ -697,7 +676,7 @@ export interface IBeforeProcessDataEvent {
export interface ITerminalProcessManager extends IDisposable {
readonly processState: ProcessState;
readonly ptyProcessReady: Promise<void>;
readonly shellProcessId: number;
readonly shellProcessId: number | undefined;
readonly remoteAuthority: string | undefined;
readonly os: OperatingSystem | undefined;
readonly userHome: string | undefined;
@@ -780,7 +759,8 @@ export interface IAvailableShellsRequest {
}
export interface IDefaultShellAndArgsRequest {
(shell: string, args: string[] | string | undefined): void;
useAutomationShell: boolean;
callback: (shell: string, args: string[] | string | undefined) => void;
}
export enum LinuxDistro {

View File

@@ -197,47 +197,61 @@ export function getDefaultShell(
lastActiveWorkspace: IWorkspaceFolder | undefined,
configurationResolverService: IConfigurationResolverService | undefined,
logService: ILogService,
useAutomationShell: boolean,
platformOverride: platform.Platform = platform.platform
): string {
const platformKey = platformOverride === platform.Platform.Windows ? 'windows' : platformOverride === platform.Platform.Mac ? 'osx' : 'linux';
const shellConfigValue = fetchSetting(`terminal.integrated.shell.${platformKey}`);
let executable = (isWorkspaceShellAllowed ? <string>shellConfigValue.value : <string>shellConfigValue.user) || (<string | null>shellConfigValue.default || defaultShell);
let maybeExecutable: string | null = null;
if (useAutomationShell) {
// If automationShell is specified, this should override the normal setting
maybeExecutable = getShellSetting(fetchSetting, isWorkspaceShellAllowed, 'automationShell', platformOverride);
}
if (!maybeExecutable) {
maybeExecutable = getShellSetting(fetchSetting, isWorkspaceShellAllowed, 'shell', platformOverride);
}
maybeExecutable = maybeExecutable || defaultShell;
// Change Sysnative to System32 if the OS is Windows but NOT WoW64. It's
// safe to assume that this was used by accident as Sysnative does not
// exist and will break the terminal in non-WoW64 environments.
if ((platformOverride === platform.Platform.Windows) && !isWoW64 && windir) {
const sysnativePath = path.join(windir, 'Sysnative').toLowerCase();
if (executable && executable.toLowerCase().indexOf(sysnativePath) === 0) {
executable = path.join(windir, 'System32', executable.substr(sysnativePath.length));
const sysnativePath = path.join(windir, 'Sysnative').replace(/\//g, '\\').toLowerCase();
if (maybeExecutable && maybeExecutable.toLowerCase().indexOf(sysnativePath) === 0) {
maybeExecutable = path.join(windir, 'System32', maybeExecutable.substr(sysnativePath.length + 1));
}
}
// Convert / to \ on Windows for convenience
if (executable && platformOverride === platform.Platform.Windows) {
executable = executable.replace(/\//g, '\\');
if (maybeExecutable && platformOverride === platform.Platform.Windows) {
maybeExecutable = maybeExecutable.replace(/\//g, '\\');
}
if (configurationResolverService) {
try {
executable = configurationResolverService.resolve(lastActiveWorkspace, executable);
maybeExecutable = configurationResolverService.resolve(lastActiveWorkspace, maybeExecutable);
} catch (e) {
logService.error(`Could not resolve terminal.integrated.shell.${platformKey}`, e);
executable = executable;
logService.error(`Could not resolve shell`, e);
maybeExecutable = maybeExecutable;
}
}
return executable;
return maybeExecutable;
}
export function getDefaultShellArgs(
fetchSetting: (key: string) => { user: string | string[] | undefined, value: string | string[] | undefined, default: string | string[] | undefined },
isWorkspaceShellAllowed: boolean,
useAutomationShell: boolean,
lastActiveWorkspace: IWorkspaceFolder | undefined,
configurationResolverService: IConfigurationResolverService | undefined,
logService: ILogService,
platformOverride: platform.Platform = platform.platform,
): string | string[] {
if (useAutomationShell) {
if (!!getShellSetting(fetchSetting, isWorkspaceShellAllowed, 'automationShell', platformOverride)) {
return [];
}
}
const platformKey = platformOverride === platform.Platform.Windows ? 'windows' : platformOverride === platform.Platform.Mac ? 'osx' : 'linux';
const shellArgsConfigValue = fetchSetting(`terminal.integrated.shellArgs.${platformKey}`);
let args = <string[] | string>((isWorkspaceShellAllowed ? shellArgsConfigValue.value : shellArgsConfigValue.user) || shellArgsConfigValue.default);
@@ -259,6 +273,18 @@ export function getDefaultShellArgs(
return args;
}
function getShellSetting(
fetchSetting: (key: string) => { user: string | string[] | undefined, value: string | string[] | undefined, default: string | string[] | undefined },
isWorkspaceShellAllowed: boolean,
type: 'automationShell' | 'shell',
platformOverride: platform.Platform = platform.platform,
): string | null {
const platformKey = platformOverride === platform.Platform.Windows ? 'windows' : platformOverride === platform.Platform.Mac ? 'osx' : 'linux';
const shellConfigValue = fetchSetting(`terminal.integrated.${type}.${platformKey}`);
const executable = (isWorkspaceShellAllowed ? <string>shellConfigValue.value : <string>shellConfigValue.user) || (<string | null>shellConfigValue.default);
return executable;
}
export function createTerminalEnvironment(
shellLaunchConfig: IShellLaunchConfig,
lastActiveWorkspace: IWorkspaceFolder | null,

View File

@@ -18,13 +18,13 @@ export class TerminalProcessExtHostProxy extends Disposable implements ITerminal
public readonly onProcessData: Event<string> = this._onProcessData.event;
private readonly _onProcessExit = this._register(new Emitter<number>());
public readonly onProcessExit: Event<number> = this._onProcessExit.event;
private readonly _onProcessReady = new Emitter<{ pid: number, cwd: string }>();
private readonly _onProcessReady = this._register(new Emitter<{ pid: number, cwd: string }>());
public get onProcessReady(): Event<{ pid: number, cwd: string }> { return this._onProcessReady.event; }
private readonly _onProcessTitleChanged = this._register(new Emitter<string>());
public readonly onProcessTitleChanged: Event<string> = this._onProcessTitleChanged.event;
private readonly _onProcessOverrideDimensions = new Emitter<ITerminalDimensions | undefined>();
private readonly _onProcessOverrideDimensions = this._register(new Emitter<ITerminalDimensions | undefined>());
public get onProcessOverrideDimensions(): Event<ITerminalDimensions | undefined> { return this._onProcessOverrideDimensions.event; }
private readonly _onProcessResolvedShellLaunchConfig = new Emitter<IShellLaunchConfig>();
private readonly _onProcessResolvedShellLaunchConfig = this._register(new Emitter<IShellLaunchConfig>());
public get onProcessResolvedShellLaunchConfig(): Event<IShellLaunchConfig> { return this._onProcessResolvedShellLaunchConfig.event; }
private readonly _onInput = this._register(new Emitter<string>());

View File

@@ -35,7 +35,7 @@ export abstract class TerminalService implements ITerminalService {
protected _isShuttingDown: boolean;
protected _terminalFocusContextKey: IContextKey<boolean>;
protected _findWidgetVisible: IContextKey<boolean>;
protected _terminalContainer: HTMLElement;
protected _terminalContainer: HTMLElement | undefined;
protected _terminalTabs: ITerminalTab[] = [];
protected _backgroundedTerminalInstances: ITerminalInstance[] = [];
protected get _terminalInstances(): ITerminalInstance[] {
@@ -123,13 +123,9 @@ export abstract class TerminalService implements ITerminalService {
protected abstract _showBackgroundTerminal(instance: ITerminalInstance): void;
public abstract createTerminal(shell?: IShellLaunchConfig, wasNewTerminalAction?: boolean): ITerminalInstance;
public abstract createInstance(terminalFocusContextKey: IContextKey<boolean>, configHelper: ITerminalConfigHelper, container: HTMLElement, shellLaunchConfig: IShellLaunchConfig): ITerminalInstance;
public abstract createInstance(container: HTMLElement, shellLaunchConfig: IShellLaunchConfig): ITerminalInstance;
public abstract setContainers(panelContainer: HTMLElement, terminalContainer: HTMLElement): void;
public createTerminalRenderer(name: string): ITerminalInstance {
return this.createTerminal({ name, isRendererOnly: true });
}
public getActiveOrCreateInstance(wasNewTerminalAction?: boolean): ITerminalInstance {
const activeInstance = this.getActiveInstance();
return activeInstance ? activeInstance : this.createTerminal(undefined, wasNewTerminalAction);

View File

@@ -73,7 +73,7 @@ export class TerminalInstanceService implements ITerminalInstanceService {
return this._storageService.getBoolean(IS_WORKSPACE_SHELL_ALLOWED_STORAGE_KEY, StorageScope.WORKSPACE, false);
}
public getDefaultShellAndArgs(platformOverride: Platform = platform): Promise<{ shell: string, args: string | string[] }> {
public getDefaultShellAndArgs(useAutomationShell: boolean, platformOverride: Platform = platform): Promise<{ shell: string, args: string | string[] }> {
const isWorkspaceShellAllowed = this._isWorkspaceShellAllowed();
const activeWorkspaceRootUri = this._historyService.getLastActiveWorkspaceRoot();
let lastActiveWorkspace = activeWorkspaceRootUri ? this._workspaceContextService.getWorkspaceFolder(activeWorkspaceRootUri) : undefined;
@@ -87,11 +87,13 @@ export class TerminalInstanceService implements ITerminalInstanceService {
lastActiveWorkspace,
this._configurationResolverService,
this._logService,
useAutomationShell,
platformOverride
);
const args = getDefaultShellArgs(
(key) => this._configurationService.inspect(key),
isWorkspaceShellAllowed,
useAutomationShell,
lastActiveWorkspace,
this._configurationResolverService,
this._logService,

View File

@@ -10,7 +10,7 @@ import * as pty from 'node-pty';
import * as fs from 'fs';
import { Event, Emitter } from 'vs/base/common/event';
import { getWindowsBuildNumber } from 'vs/workbench/contrib/terminal/node/terminal';
import { IDisposable } from 'vs/base/common/lifecycle';
import { Disposable } from 'vs/base/common/lifecycle';
import { IShellLaunchConfig, ITerminalChildProcess, SHELL_PATH_INVALID_EXIT_CODE, SHELL_PATH_DIRECTORY_EXIT_CODE, SHELL_CWD_INVALID_EXIT_CODE } from 'vs/workbench/contrib/terminal/common/terminal';
import { exec } from 'child_process';
import { ILogService } from 'vs/platform/log/common/log';
@@ -18,23 +18,23 @@ import { stat } from 'vs/base/node/pfs';
import { findExecutable } from 'vs/workbench/contrib/terminal/node/terminalEnvironment';
import { URI } from 'vs/base/common/uri';
export class TerminalProcess implements ITerminalChildProcess, IDisposable {
private _exitCode: number;
export class TerminalProcess extends Disposable implements ITerminalChildProcess {
private _exitCode: number | undefined;
private _closeTimeout: any;
private _ptyProcess: pty.IPty | undefined;
private _currentTitle: string = '';
private _processStartupComplete: Promise<void>;
private _processStartupComplete: Promise<void> | undefined;
private _isDisposed: boolean = false;
private _titleInterval: NodeJS.Timer | null = null;
private _initialCwd: string;
private readonly _onProcessData = new Emitter<string>();
private readonly _onProcessData = this._register(new Emitter<string>());
public get onProcessData(): Event<string> { return this._onProcessData.event; }
private readonly _onProcessExit = new Emitter<number>();
private readonly _onProcessExit = this._register(new Emitter<number>());
public get onProcessExit(): Event<number> { return this._onProcessExit.event; }
private readonly _onProcessReady = new Emitter<{ pid: number, cwd: string }>();
private readonly _onProcessReady = this._register(new Emitter<{ pid: number, cwd: string }>());
public get onProcessReady(): Event<{ pid: number, cwd: string }> { return this._onProcessReady.event; }
private readonly _onProcessTitleChanged = new Emitter<string>();
private readonly _onProcessTitleChanged = this._register(new Emitter<string>());
public get onProcessTitleChanged(): Event<string> { return this._onProcessTitleChanged.event; }
constructor(
@@ -46,6 +46,7 @@ export class TerminalProcess implements ITerminalChildProcess, IDisposable {
windowsEnableConpty: boolean,
@ILogService private readonly _logService: ILogService
) {
super();
let shellName: string;
if (os.platform() === 'win32') {
shellName = path.basename(shellLaunchConfig.executable || '');
@@ -65,7 +66,8 @@ export class TerminalProcess implements ITerminalChildProcess, IDisposable {
cols,
rows,
experimentalUseConpty: useConpty,
conptyInheritCursor: true
// This option will force conpty to not redraw the whole viewport on launch
conptyInheritCursor: useConpty && !!shellLaunchConfig.initialText
};
const cwdVerification = stat(cwd).then(async stat => {
@@ -87,7 +89,9 @@ export class TerminalProcess implements ITerminalChildProcess, IDisposable {
}, async (err) => {
if (err && err.code === 'ENOENT') {
let cwd = shellLaunchConfig.cwd instanceof URI ? shellLaunchConfig.cwd.path : shellLaunchConfig.cwd!;
const executable = await findExecutable(shellLaunchConfig.executable!, cwd);
// Try to get path
const envPaths: string[] | undefined = (shellLaunchConfig.env && shellLaunchConfig.env.PATH) ? shellLaunchConfig.env.PATH.split(path.delimiter) : undefined;
const executable = await findExecutable(shellLaunchConfig.executable!, cwd, envPaths);
if (!executable) {
return Promise.reject(SHELL_PATH_INVALID_EXIT_CODE);
}
@@ -143,6 +147,7 @@ export class TerminalProcess implements ITerminalChildProcess, IDisposable {
this._onProcessExit.dispose();
this._onProcessReady.dispose();
this._onProcessTitleChanged.dispose();
super.dispose();
}
private _setupTitlePolling(ptyProcess: pty.IPty) {
@@ -172,7 +177,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;
}
@@ -186,7 +191,7 @@ export class TerminalProcess implements ITerminalChildProcess, IDisposable {
} catch (ex) {
// Swallow, the pty has already been killed
}
this._onProcessExit.fire(this._exitCode);
this._onProcessExit.fire(this._exitCode || 0);
this.dispose();
});
}

View File

@@ -8,6 +8,7 @@ import { Emitter, Event } from 'vs/base/common/event';
import { ITerminalInstance, IWindowsShellHelper } from 'vs/workbench/contrib/terminal/common/terminal';
import { Terminal as XTermTerminal } from 'xterm';
import WindowsProcessTreeType = require('windows-process-tree');
import { Disposable } from 'vs/base/common/lifecycle';
const SHELL_EXECUTABLES = [
'cmd.exe',
@@ -24,17 +25,19 @@ const SHELL_EXECUTABLES = [
let windowsProcessTree: typeof WindowsProcessTreeType;
export class WindowsShellHelper implements IWindowsShellHelper {
private _onCheckShell: Emitter<Promise<string> | undefined>;
export class WindowsShellHelper extends Disposable implements IWindowsShellHelper {
private _onCheckShell: Emitter<Promise<string> | undefined> = this._register(new Emitter<Promise<string> | undefined>());
private _isDisposed: boolean;
private _currentRequest: Promise<string> | null;
private _newLineFeed: boolean;
private _currentRequest: Promise<string> | undefined;
private _newLineFeed: boolean = false;
public constructor(
private _rootProcessId: number,
private _terminalInstance: ITerminalInstance,
private _xterm: XTermTerminal
) {
super();
if (!platform.isWindows) {
throw new Error(`WindowsShellHelper cannot be instantiated on ${platform.platform}`);
}
@@ -47,7 +50,6 @@ export class WindowsShellHelper implements IWindowsShellHelper {
}
windowsProcessTree = mod;
this._onCheckShell = new Emitter<Promise<string>>();
// 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 +67,7 @@ export class WindowsShellHelper implements IWindowsShellHelper {
this._xterm.onCursorMove(() => {
if (this._newLineFeed) {
this._onCheckShell.fire(undefined);
this._newLineFeed = false;
}
});
@@ -111,6 +114,7 @@ export class WindowsShellHelper implements IWindowsShellHelper {
public dispose(): void {
this._isDisposed = true;
super.dispose();
}
/**
@@ -127,7 +131,7 @@ export class WindowsShellHelper implements IWindowsShellHelper {
this._currentRequest = new Promise<string>(resolve => {
windowsProcessTree.getProcessTree(this._rootProcessId, (tree) => {
const name = this.traverseTree(tree);
this._currentRequest = null;
this._currentRequest = undefined;
resolve(name);
});
});

View File

@@ -5,7 +5,7 @@
import * as assert from 'assert';
import { Terminal, TerminalCore } from 'xterm';
import { TerminalCommandTracker } from 'vs/workbench/contrib/terminal/browser/terminalCommandTracker';
import { CommandTrackerAddon } from 'vs/workbench/contrib/terminal/browser/addons/commandTrackerAddon';
import { isWindows } from 'vs/base/common/platform';
interface TestTerminalCore extends TerminalCore {

View File

@@ -65,7 +65,7 @@ interface LinkFormatInfo {
suite('Workbench - TerminalLinkHandler', () => {
suite('localLinkRegex', () => {
test('Windows', () => {
const terminalLinkHandler = new TestTerminalLinkHandler(new TestXterm(), {
const terminalLinkHandler = new TestTerminalLinkHandler(new TestXterm() as any, {
os: OperatingSystem.Windows,
userHome: ''
} as any, null!, null!, null!, null!, new MockTerminalInstanceService(), null!);
@@ -141,7 +141,7 @@ suite('Workbench - TerminalLinkHandler', () => {
});
test('Linux', () => {
const terminalLinkHandler = new TestTerminalLinkHandler(new TestXterm(), {
const terminalLinkHandler = new TestTerminalLinkHandler(new TestXterm() as any, {
os: OperatingSystem.Linux,
userHome: ''
} as any, null!, null!, null!, null!, new MockTerminalInstanceService(), null!);
@@ -209,7 +209,7 @@ suite('Workbench - TerminalLinkHandler', () => {
suite('preprocessPath', () => {
test('Windows', () => {
const linkHandler = new TestTerminalLinkHandler(new TestXterm(), {
const linkHandler = new TestTerminalLinkHandler(new TestXterm() as any, {
os: OperatingSystem.Windows,
userHome: 'C:\\Users\\Me'
} as any, null!, null!, null!, null!, new MockTerminalInstanceService(), null!);
@@ -222,7 +222,7 @@ suite('Workbench - TerminalLinkHandler', () => {
assert.equal(linkHandler.preprocessPath('C:\\absolute\\path\\file5'), 'C:\\absolute\\path\\file5');
});
test('Windows - spaces', () => {
const linkHandler = new TestTerminalLinkHandler(new TestXterm(), {
const linkHandler = new TestTerminalLinkHandler(new TestXterm() as any, {
os: OperatingSystem.Windows,
userHome: 'C:\\Users\\M e'
} as any, null!, null!, null!, null!, new MockTerminalInstanceService(), null!);
@@ -236,7 +236,7 @@ suite('Workbench - TerminalLinkHandler', () => {
});
test('Linux', () => {
const linkHandler = new TestTerminalLinkHandler(new TestXterm(), {
const linkHandler = new TestTerminalLinkHandler(new TestXterm() as any, {
os: OperatingSystem.Linux,
userHome: '/home/me'
} as any, null!, null!, null!, null!, new MockTerminalInstanceService(), null!);
@@ -249,7 +249,7 @@ suite('Workbench - TerminalLinkHandler', () => {
});
test('No Workspace', () => {
const linkHandler = new TestTerminalLinkHandler(new TestXterm(), {
const linkHandler = new TestTerminalLinkHandler(new TestXterm() as any, {
os: OperatingSystem.Linux,
userHome: '/home/me'
} as any, null!, null!, null!, null!, new MockTerminalInstanceService(), null!);
@@ -263,7 +263,7 @@ suite('Workbench - TerminalLinkHandler', () => {
test('gitDiffLinkRegex', () => {
// The platform is irrelevant because the links generated by Git are the same format regardless of platform
const linkHandler = new TestTerminalLinkHandler(new TestXterm(), {
const linkHandler = new TestTerminalLinkHandler(new TestXterm() as any, {
os: OperatingSystem.Linux,
userHome: ''
} as any, null!, null!, null!, null!, new MockTerminalInstanceService(), null!);

View File

@@ -128,4 +128,48 @@ suite('Workbench - TerminalEnvironment', () => {
assertPathsMatch(terminalEnvironment.getCwd({ executable: undefined, args: [], ignoreConfigurationCwd: true }, '/userHome/', undefined, undefined, Uri.file('/bar'), '/foo'), '/bar');
});
});
suite('getDefaultShell', () => {
test('should change Sysnative to System32 in non-WoW64 systems', () => {
const shell = terminalEnvironment.getDefaultShell(key => {
return ({
'terminal.integrated.shell.windows': { user: 'C:\\Windows\\Sysnative\\cmd.exe', value: undefined, default: undefined }
} as any)[key];
}, false, 'DEFAULT', false, 'C:\\Windows', undefined, undefined, {} as any, false, platform.Platform.Windows);
assert.equal(shell, 'C:\\Windows\\System32\\cmd.exe');
});
test('should not change Sysnative to System32 in WoW64 systems', () => {
const shell = terminalEnvironment.getDefaultShell(key => {
return ({
'terminal.integrated.shell.windows': { user: 'C:\\Windows\\Sysnative\\cmd.exe', value: undefined, default: undefined }
} as any)[key];
}, false, 'DEFAULT', true, 'C:\\Windows', undefined, undefined, {} as any, false, platform.Platform.Windows);
assert.equal(shell, 'C:\\Windows\\Sysnative\\cmd.exe');
});
test('should use automationShell when specified', () => {
const shell1 = terminalEnvironment.getDefaultShell(key => {
return ({
'terminal.integrated.shell.windows': { user: 'shell', value: undefined, default: undefined },
'terminal.integrated.automationShell.windows': { user: undefined, value: undefined, default: undefined }
} as any)[key];
}, false, 'DEFAULT', false, 'C:\\Windows', undefined, undefined, {} as any, false, platform.Platform.Windows);
assert.equal(shell1, 'shell', 'automationShell was false');
const shell2 = terminalEnvironment.getDefaultShell(key => {
return ({
'terminal.integrated.shell.windows': { user: 'shell', value: undefined, default: undefined },
'terminal.integrated.automationShell.windows': { user: undefined, value: undefined, default: undefined }
} as any)[key];
}, false, 'DEFAULT', false, 'C:\\Windows', undefined, undefined, {} as any, true, platform.Platform.Windows);
assert.equal(shell2, 'shell', 'automationShell was true');
const shell3 = terminalEnvironment.getDefaultShell(key => {
return ({
'terminal.integrated.shell.windows': { user: 'shell', value: undefined, default: undefined },
'terminal.integrated.automationShell.windows': { user: 'automationShell', value: undefined, default: undefined }
} as any)[key];
}, false, 'DEFAULT', false, 'C:\\Windows', undefined, undefined, {} as any, true, platform.Platform.Windows);
assert.equal(shell3, 'automationShell', 'automationShell was true and specified in settings');
});
});
});