diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/window.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/window.test.ts index d57851e58af..449cc78536a 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/window.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/window.test.ts @@ -6,7 +6,7 @@ 'use strict'; import * as assert from 'assert'; -import { workspace, window, commands, ViewColumn, TextEditorViewColumnChangeEvent, Uri, Selection, Position, CancellationTokenSource, TextEditorSelectionChangeKind } from 'vscode'; +import { workspace, window, commands, ViewColumn, TextEditorViewColumnChangeEvent, Uri, Selection, Position, CancellationTokenSource, TextEditorSelectionChangeKind, Terminal } from 'vscode'; import { join } from 'path'; import { closeAllEditors, pathEquals, createRandomFile } from '../utils'; @@ -557,37 +557,143 @@ suite('window namespace tests', () => { }); }); - test('createTerminal, Terminal.name', () => { - const terminal = window.createTerminal('foo'); - assert.equal(terminal.name, 'foo'); + suite('Terminal', () => { + test('createTerminal, Terminal.name', () => { + const terminal = window.createTerminal('foo'); + assert.equal(terminal.name, 'foo'); - assert.throws(() => { - (terminal).name = 'bar'; - }, 'Terminal.name should be readonly'); - }); - - test('terminal, sendText immediately after createTerminal should not throw', () => { - const terminal = window.createTerminal(); - assert.doesNotThrow(terminal.sendText.bind(terminal, 'echo "foo"')); - }); - - test('terminal, onDidCloseTerminal event fires when terminal is disposed', (done) => { - const terminal = window.createTerminal(); - window.onDidCloseTerminal((eventTerminal) => { - assert.equal(terminal, eventTerminal); - done(); + assert.throws(() => { + (terminal).name = 'bar'; + }, 'Terminal.name should be readonly'); + terminal.dispose(); }); - terminal.dispose(); - }); - test('terminal, processId immediately after createTerminal should fetch the pid', (done) => { - window.createTerminal().processId.then(id => { - assert.ok(id > 0); - done(); + test('sendText immediately after createTerminal should not throw', () => { + const terminal = window.createTerminal(); + assert.doesNotThrow(terminal.sendText.bind(terminal, 'echo "foo"')); + terminal.dispose(); }); - }); - test('terminal, name should set terminal.name', () => { - assert.equal(window.createTerminal('foo').name, 'foo'); + test('onDidCloseTerminal event fires when terminal is disposed', (done) => { + const terminal = window.createTerminal(); + const reg = window.onDidCloseTerminal((eventTerminal) => { + assert.equal(terminal, eventTerminal); + reg.dispose(); + done(); + }); + terminal.dispose(); + }); + + test('processId immediately after createTerminal should fetch the pid', (done) => { + const terminal = window.createTerminal(); + terminal.processId.then(id => { + assert.ok(id > 0); + terminal.dispose(); + done(); + }); + }); + + test('name in constructor should set terminal.name', () => { + const terminal = window.createTerminal('a'); + assert.equal(terminal.name, 'a'); + terminal.dispose(); + }); + + test('onDidOpenTerminal should fire when a terminal is created', (done) => { + const reg1 = window.onDidOpenTerminal(term => { + assert.equal(term.name, 'b'); + reg1.dispose(); + const reg2 = window.onDidCloseTerminal(() => { + reg2.dispose(); + done(); + }); + terminal.dispose(); + }); + const terminal = window.createTerminal('b'); + }); + + test('createTerminalRenderer should fire onDidOpenTerminal and onDidCloseTerminal', (done) => { + const reg1 = window.onDidOpenTerminal(term => { + assert.equal(term.name, 'c'); + reg1.dispose(); + const reg2 = window.onDidCloseTerminal(() => { + reg2.dispose(); + done(); + }); + term.dispose(); + }); + window.createTerminalRenderer('c'); + }); + + test('terminal renderers should get maximum dimensions set when shown', (done) => { + let terminal: Terminal; + const reg1 = window.onDidOpenTerminal(term => { + reg1.dispose(); + term.show(); + terminal = term; + }); + const renderer = window.createTerminalRenderer('foo'); + const reg2 = renderer.onDidChangeMaximumDimensions(dimensions => { + assert.ok(dimensions.cols > 0); + assert.ok(dimensions.rows > 0); + reg2.dispose(); + const reg3 = window.onDidCloseTerminal(() => { + reg3.dispose(); + done(); + }); + terminal.dispose(); + }); + }); + + test('TerminalRenderer.write should fire Terminal.onData', (done) => { + const reg1 = window.onDidOpenTerminal(terminal => { + reg1.dispose(); + const reg2 = terminal.onData(data => { + assert.equal(data, 'bar'); + reg2.dispose(); + const reg3 = window.onDidCloseTerminal(() => { + reg3.dispose(); + done(); + }); + terminal.dispose(); + }); + renderer.write('bar'); + }); + const renderer = window.createTerminalRenderer('foo'); + }); + + test('Terminal.sendText should fire Termnial.onInput', (done) => { + const reg1 = window.onDidOpenTerminal(terminal => { + reg1.dispose(); + const reg2 = renderer.onInput(data => { + assert.equal(data, 'bar'); + reg2.dispose(); + const reg3 = window.onDidCloseTerminal(() => { + reg3.dispose(); + done(); + }); + terminal.dispose(); + }); + terminal.sendText('bar', false); + }); + const renderer = window.createTerminalRenderer('foo'); + }); + + test('onDidChangeActiveTerminal should fire when new terminals are created', (done) => { + const reg1 = window.onDidChangeActiveTerminal((active: Terminal | undefined) => { + assert.equal(active, terminal); + assert.equal(active, window.activeTerminal); + reg1.dispose(); + const reg2 = window.onDidChangeActiveTerminal((active: Terminal | undefined) => { + assert.equal(active, undefined); + assert.equal(active, window.activeTerminal); + reg2.dispose(); + done(); + }); + terminal.dispose(); + }); + const terminal = window.createTerminal(); + terminal.show(); + }); }); }); diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 2a67cc28100..44c776e8f4e 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -339,11 +339,123 @@ declare module 'vscode' { /** * Fires when the terminal's pty slave pseudo-device is written to. In other words, this * provides access to the raw data stream from the process running within the terminal, - * including ANSI sequences. + * including VT sequences. */ onData: Event; } + /** + * Represents the dimensions of a terminal. + */ + export interface TerminalDimensions { + /** + * The number of columns in the terminal. + */ + cols: number; + + /** + * The number of rows in the terminal. + */ + rows: number; + } + + /** + * Represents a terminal without a process where all interaction and output in the terminal is + * controlled by an extension. This is similar to an output window but has the same VT sequence + * compatility as the regular terminal. + * + * Note that an instance of [Terminal](#Terminal) will be created when a TerminalRenderer is + * created with all its APIs available for use by extensions. When using the Terminal object + * of a TerminalRenderer it acts just like normal only the extension that created the + * TerminalRenderer essentially acts as a process. For example when an + * [Terminal.onData](#Terminal.onData) listener is registered, that will fire when + * [TerminalRenderer.write](#TerminalRenderer.write) is called. Similarly when + * [Terminal.sendText](#Terminal.sendText) is triggered that will fire the + * [TerminalRenderer.onInput](#TerminalRenderer.onInput) event. + * + * **Example:** Create a terminal renderer, show it and write hello world in red + * ```typescript + * const renderer = window.createTerminalRenderer('foo'); + * renderer.terminal.then(t => t.show()); + * renderer.write('\x1b[31mHello world\x1b[0m'); + * ``` + */ + export interface TerminalRenderer { + /** + * The name of the terminal, this will appear in the terminal selector. + */ + name: string; + + /** + * The dimensions of the terminal, the rows and columns of the terminal can only be set to + * a value smaller than the maximum value, if this is undefined the terminal will auto fit + * to the maximum value [maximumDimensions](TerminalRenderer.maximumDimensions). + * + * **Example:** Override the dimensions of a TerminalRenderer to 20 columns and 10 rows + * ```typescript + * terminalRenderer.dimensions = { + * cols: 20, + * rows: 10 + * }; + * ``` + */ + dimensions: TerminalDimensions; + + /** + * The maximum dimensions of the terminal, this will be undefined immediately after a + * terminal renderer is created and also until the terminal becomes visible in the UI. + * Listen to [onDidChangeMaximumDimensions](TerminalRenderer.onDidChangeMaximumDimensions) + * to get notified when this value changes. + */ + readonly maximumDimensions: TerminalDimensions; + + /** + * The corressponding [Terminal](#Terminal) for this TerminalRenderer. + */ + readonly terminal: Thenable; + + /** + * Write text to the terminal. Unlike [Terminal.sendText](#Terminal.sendText) which sends + * text to the underlying _process_, this will write the text to the terminal itself. + * + * **Example:** Write red text to the terminal + * ```typescript + * terminalRenderer.write('\x1b[31mHello world\x1b[0m'); + * ``` + * + * **Example:** Move the cursor to the 10th row and 20th column and write an asterisk + * ```typescript + * terminalRenderer.write('\x1b[10;20H*'); + * ``` + * + * @param text The text to write. + */ + write(text: string): void; + + /** + * An event which fires on keystrokes in the terminal or when an extension calls + * [Terminal.sendText](#Terminal.sendText). Keystrokes are converted into their + * corresponding VT sequence representation. + * + * **Example:** Simulate interaction with the terminal from an outside extension or a + * workbench command such as `workbench.action.terminal.runSelectedText` + * ```typescript + * const terminalRenderer = window.createTerminalRenderer('test'); + * terminalRenderer.onInput(data => { + * cosole.log(data); // 'Hello world' + * }); + * terminalRenderer.terminal.then(t => t.sendText('Hello world')); + * ``` + */ + onInput: Event; + + /** + * An event which fires when the [maximum dimensions](#TerminalRenderer.maimumDimensions) of + * the terminal renderer change. + */ + onDidChangeMaximumDimensions: Event; + } + export namespace window { /** * The currently opened terminals or an empty array. @@ -352,11 +464,33 @@ declare module 'vscode' { */ export let terminals: Terminal[]; + /** + * The currently active terminal or `undefined`. The active terminal is the one that + * currently has focus or most recently had focus. + * + * @readonly + */ + export let activeTerminal: Terminal | undefined; + + /** + * An [event](#Event) which fires when the [active terminal](#window.activeTerminal) + * has changed. *Note* that the event also fires when the active editor changes + * to `undefined`. + */ + export const onDidChangeActiveTerminal: Event; + /** * An [event](#Event) which fires when a terminal has been created, either through the * [createTerminal](#window.createTerminal) API or commands. */ export const onDidOpenTerminal: Event; + + /** + * Create a [TerminalRenderer](#TerminalRenderer). + * + * @param name The name of the terminal renderer, this shows up in the terminal selector. + */ + export function createTerminalRenderer(name: string): TerminalRenderer; } //#endregion diff --git a/src/vs/workbench/api/electron-browser/mainThreadTerminalService.ts b/src/vs/workbench/api/electron-browser/mainThreadTerminalService.ts index 58e3ab9786d..d953dcb6a8c 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadTerminalService.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadTerminalService.ts @@ -5,7 +5,7 @@ 'use strict'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; -import { ITerminalService, ITerminalInstance, IShellLaunchConfig, ITerminalProcessExtHostProxy, ITerminalProcessExtHostRequest } from 'vs/workbench/parts/terminal/common/terminal'; +import { ITerminalService, ITerminalInstance, IShellLaunchConfig, ITerminalProcessExtHostProxy, ITerminalProcessExtHostRequest, ITerminalDimensions, EXT_HOST_CREATION_DELAY } from 'vs/workbench/parts/terminal/common/terminal'; import { TPromise } from 'vs/base/common/winjs.base'; import { ExtHostContext, ExtHostTerminalServiceShape, MainThreadTerminalServiceShape, MainContext, IExtHostContext, ShellLaunchConfigDto } from '../node/extHost.protocol'; import { extHostNamedCustomer } from 'vs/workbench/api/electron-browser/extHostCustomers'; @@ -16,22 +16,23 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape private _proxy: ExtHostTerminalServiceShape; private _toDispose: IDisposable[] = []; private _terminalProcesses: { [id: number]: ITerminalProcessExtHostProxy } = {}; - private _dataListeners: { [id: number]: IDisposable } = {}; constructor( extHostContext: IExtHostContext, @ITerminalService private terminalService: ITerminalService ) { this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostTerminalService); - this._toDispose.push(terminalService.onInstanceCreated((terminalInstance) => { + this._toDispose.push(terminalService.onInstanceCreated((instance) => { // Delay this message so the TerminalInstance constructor has a chance to finish and // return the ID normally to the extension host. The ID that is passed here will be used // to register non-extension API terminals in the extension host. - setTimeout(() => this._onTerminalOpened(terminalInstance), 100); + setTimeout(() => this._onTerminalOpened(instance), EXT_HOST_CREATION_DELAY); })); - this._toDispose.push(terminalService.onInstanceDisposed(terminalInstance => this._onTerminalDisposed(terminalInstance))); - this._toDispose.push(terminalService.onInstanceProcessIdReady(terminalInstance => this._onTerminalProcessIdReady(terminalInstance))); + this._toDispose.push(terminalService.onInstanceDisposed(instance => this._onTerminalDisposed(instance))); + this._toDispose.push(terminalService.onInstanceProcessIdReady(instance => this._onTerminalProcessIdReady(instance))); + this._toDispose.push(terminalService.onInstanceDimensionsChanged(instance => this._onInstanceDimensionsChanged(instance))); this._toDispose.push(terminalService.onInstanceRequestExtHostProcess(request => this._onTerminalRequestExtHostProcess(request))); + this._toDispose.push(terminalService.onActiveInstanceChanged(instance => this._onActiveTerminalChanged(instance ? instance.id : undefined))); // Set initial ext host state this.terminalService.terminalInstances.forEach(t => { @@ -60,8 +61,13 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape return TPromise.as(this.terminalService.createTerminal(shellLaunchConfig).id); } + public $createTerminalRenderer(name: string): TPromise { + const instance = this.terminalService.createTerminalRenderer(name); + return TPromise.as(instance.id); + } + public $show(terminalId: number, preserveFocus: boolean): void { - let terminalInstance = this.terminalService.getInstanceFromId(terminalId); + const terminalInstance = this.terminalService.getInstanceFromId(terminalId); if (terminalInstance) { this.terminalService.setActiveInstance(terminalInstance); this.terminalService.showPanel(!preserveFocus); @@ -75,31 +81,68 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape } public $dispose(terminalId: number): void { - let terminalInstance = this.terminalService.getInstanceFromId(terminalId); + const terminalInstance = this.terminalService.getInstanceFromId(terminalId); if (terminalInstance) { terminalInstance.dispose(); } } + public $terminalRendererWrite(terminalId: number, text: string): void { + const terminalInstance = this.terminalService.getInstanceFromId(terminalId); + if (terminalInstance && terminalInstance.shellLaunchConfig.isRendererOnly) { + terminalInstance.write(text); + } + } + + public $terminalRendererSetName(terminalId: number, name: string): void { + const terminalInstance = this.terminalService.getInstanceFromId(terminalId); + if (terminalInstance && terminalInstance.shellLaunchConfig.isRendererOnly) { + terminalInstance.setTitle(name, false); + } + } + + public $terminalRendererSetDimensions(terminalId: number, dimensions: ITerminalDimensions): void { + const terminalInstance = this.terminalService.getInstanceFromId(terminalId); + if (terminalInstance && terminalInstance.shellLaunchConfig.isRendererOnly) { + terminalInstance.setDimensions(dimensions); + } + } + + public $terminalRendererRegisterOnInputListener(terminalId: number): void { + const terminalInstance = this.terminalService.getInstanceFromId(terminalId); + if (terminalInstance) { + terminalInstance.addDisposable(terminalInstance.onRendererInput(data => this._onTerminalRendererInput(terminalId, data))); + } + } + public $sendText(terminalId: number, text: string, addNewLine: boolean): void { - let terminalInstance = this.terminalService.getInstanceFromId(terminalId); + const terminalInstance = this.terminalService.getInstanceFromId(terminalId); if (terminalInstance) { terminalInstance.sendText(text, addNewLine); } } public $registerOnDataListener(terminalId: number): void { - let terminalInstance = this.terminalService.getInstanceFromId(terminalId); + const terminalInstance = this.terminalService.getInstanceFromId(terminalId); if (terminalInstance) { - this._dataListeners[terminalId] = terminalInstance.onData(data => this._onTerminalData(terminalId, data)); - terminalInstance.onDisposed(instance => delete this._dataListeners[terminalId]); + terminalInstance.addDisposable(terminalInstance.onData(data => { + this._onTerminalData(terminalId, data); + })); } } + private _onActiveTerminalChanged(terminalId: number | undefined): void { + this._proxy.$acceptActiveTerminalChanged(terminalId); + } + private _onTerminalData(terminalId: number, data: string): void { this._proxy.$acceptTerminalProcessData(terminalId, data); } + private _onTerminalRendererInput(terminalId: number, data: string): void { + this._proxy.$acceptTerminalRendererInput(terminalId, data); + } + private _onTerminalDisposed(terminalInstance: ITerminalInstance): void { this._proxy.$acceptTerminalClosed(terminalInstance.id); } @@ -112,6 +155,14 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape this._proxy.$acceptTerminalProcessId(terminalInstance.id, terminalInstance.processId); } + private _onInstanceDimensionsChanged(instance: ITerminalInstance): void { + // Only send the dimensions if the terminal is a renderer only as there is no API to access + // dimensions on a plain Terminal. + if (instance.shellLaunchConfig.isRendererOnly) { + this._proxy.$acceptTerminalRendererDimensions(instance.id, instance.cols, instance.rows); + } + } + private _onTerminalRequestExtHostProcess(request: ITerminalProcessExtHostRequest): void { this._terminalProcesses[request.proxy.terminalId] = request.proxy; const shellLaunchConfigDto: ShellLaunchConfigDto = { diff --git a/src/vs/workbench/api/node/extHost.api.impl.ts b/src/vs/workbench/api/node/extHost.api.impl.ts index 2e7cc86e8db..64c5d869756 100644 --- a/src/vs/workbench/api/node/extHost.api.impl.ts +++ b/src/vs/workbench/api/node/extHost.api.impl.ts @@ -331,6 +331,9 @@ export function createApiFactory( get visibleTextEditors() { return extHostEditors.getVisibleTextEditors(); }, + get activeTerminal() { + return proposedApiFunction(extension, extHostTerminalService.activeTerminal); + }, get terminals() { return proposedApiFunction(extension, extHostTerminalService.terminals); }, @@ -372,6 +375,9 @@ export function createApiFactory( onDidOpenTerminal: proposedApiFunction(extension, (listener, thisArg?, disposables?) => { return extHostTerminalService.onDidOpenTerminal(listener, thisArg, disposables); }), + onDidChangeActiveTerminal: proposedApiFunction(extension, (listener, thisArg?, disposables?) => { + return extHostTerminalService.onDidChangeActiveTerminal(listener, thisArg, disposables); + }), get state() { return extHostWindow.state; }, @@ -427,6 +433,9 @@ export function createApiFactory( } return extHostTerminalService.createTerminal(nameOrOptions, shellPath, shellArgs); }, + createTerminalRenderer(name: string): vscode.TerminalRenderer { + return extHostTerminalService.createTerminalRenderer(name); + }, registerTreeDataProvider(viewId: string, treeDataProvider: vscode.TreeDataProvider): vscode.Disposable { return extHostTreeViews.registerTreeDataProvider(viewId, treeDataProvider); }, diff --git a/src/vs/workbench/api/node/extHost.protocol.ts b/src/vs/workbench/api/node/extHost.protocol.ts index adbb6dd1a0b..c43d5a27b6e 100644 --- a/src/vs/workbench/api/node/extHost.protocol.ts +++ b/src/vs/workbench/api/node/extHost.protocol.ts @@ -49,6 +49,7 @@ import { ISingleEditOperation } from 'vs/editor/common/model'; import { IPatternInfo, IRawSearchQuery, IRawFileMatch2, ISearchCompleteStats } from 'vs/platform/search/common/search'; import { LogLevel } from 'vs/platform/log/common/log'; import { TaskExecutionDTO, TaskDTO, TaskHandleDTO, TaskFilterDTO, TaskProcessStartedDTO, TaskProcessEndedDTO, TaskSystemInfoDTO } from 'vs/workbench/api/shared/tasks'; +import { ITerminalDimensions } from 'vs/workbench/parts/terminal/common/terminal'; export interface IEnvironment { isExtensionDevelopmentDebug: boolean; @@ -324,16 +325,24 @@ export interface MainThreadProgressShape extends IDisposable { export interface MainThreadTerminalServiceShape extends IDisposable { $createTerminal(name?: string, shellPath?: string, shellArgs?: string[], cwd?: string, env?: { [key: string]: string }, waitOnExit?: boolean): TPromise; + $createTerminalRenderer(name: string): TPromise; $dispose(terminalId: number): void; $hide(terminalId: number): void; $sendText(terminalId: number, text: string, addNewLine: boolean): void; $show(terminalId: number, preserveFocus: boolean): void; $registerOnDataListener(terminalId: number): void; + // Process $sendProcessTitle(terminalId: number, title: string): void; $sendProcessData(terminalId: number, data: string): void; $sendProcessPid(terminalId: number, pid: number): void; $sendProcessExit(terminalId: number, exitCode: number): void; + + // Renderer + $terminalRendererSetName(terminalId: number, name: string): void; + $terminalRendererSetDimensions(terminalId: number, dimensions: ITerminalDimensions): void; + $terminalRendererWrite(terminalId: number, text: string): void; + $terminalRendererRegisterOnInputListener(terminalId: number): void; } export interface TransferQuickPickItems extends IQuickPickItem { @@ -841,8 +850,11 @@ export interface ShellLaunchConfigDto { export interface ExtHostTerminalServiceShape { $acceptTerminalClosed(id: number): void; $acceptTerminalOpened(id: number, name: string): void; + $acceptActiveTerminalChanged(id: number | null): void; $acceptTerminalProcessId(id: number, processId: number): void; $acceptTerminalProcessData(id: number, data: string): void; + $acceptTerminalRendererInput(id: number, data: string): void; + $acceptTerminalRendererDimensions(id: number, cols: number, rows: number): void; $createProcess(id: number, shellLaunchConfig: ShellLaunchConfigDto, cols: number, rows: number): void; $acceptProcessInput(id: number, data: string): void; $acceptProcessResize(id: number, cols: number, rows: number): void; diff --git a/src/vs/workbench/api/node/extHostTerminalService.ts b/src/vs/workbench/api/node/extHostTerminalService.ts index c4f2e996461..26284ca0a98 100644 --- a/src/vs/workbench/api/node/extHostTerminalService.ts +++ b/src/vs/workbench/api/node/extHostTerminalService.ts @@ -15,13 +15,64 @@ import { ExtHostTerminalServiceShape, MainContext, MainThreadTerminalServiceShap import { IMessageFromTerminalProcess } from 'vs/workbench/parts/terminal/node/terminal'; import { ExtHostConfiguration } from 'vs/workbench/api/node/extHostConfiguration'; import { ILogService } from 'vs/platform/log/common/log'; +import { EXT_HOST_CREATION_DELAY } from 'vs/workbench/parts/terminal/common/terminal'; -export class ExtHostTerminal implements vscode.Terminal { - private _name: string; - private _id: number; - private _proxy: MainThreadTerminalServiceShape; - private _disposed: boolean; - private _queuedRequests: ApiRequest[]; +const RENDERER_NO_PROCESS_ID = -1; + +export class BaseExtHostTerminal { + public _id: number; + protected _idPromise: Promise; + private _idPromiseComplete: (value: number) => any; + private _disposed: boolean = false; + private _queuedRequests: ApiRequest[] = []; + + constructor( + protected _proxy: MainThreadTerminalServiceShape, + id?: number + ) { + this._idPromise = new Promise(c => { + if (id !== undefined) { + this._id = id; + c(id); + } else { + this._idPromiseComplete = c; + } + }); + } + + public dispose(): void { + if (!this._disposed) { + this._disposed = true; + this._queueApiRequest(this._proxy.$dispose, []); + } + } + + protected _checkDisposed() { + if (this._disposed) { + throw new Error('Terminal has already been disposed'); + } + } + + protected _queueApiRequest(callback: (...args: any[]) => void, args: any[]): void { + const request: ApiRequest = new ApiRequest(callback, args); + if (!this._id) { + this._queuedRequests.push(request); + return; + } + request.run(this._proxy, this._id); + } + + protected _runQueuedRequests(id: number): void { + this._id = id; + this._idPromiseComplete(id); + this._queuedRequests.forEach((r) => { + r.run(this._proxy, this._id); + }); + this._queuedRequests.length = 0; + } +} + +export class ExtHostTerminal extends BaseExtHostTerminal implements vscode.Terminal { private _pidPromise: Promise; private _pidPromiseComplete: (value: number) => any; @@ -34,17 +85,17 @@ export class ExtHostTerminal implements vscode.Terminal { constructor( proxy: MainThreadTerminalServiceShape, - name: string = '', - id?: number + private _name: string, + id?: number, + pid?: number ) { - this._proxy = proxy; - this._name = name; - if (id) { - this._id = id; - } - this._queuedRequests = []; + super(proxy, id); this._pidPromise = new Promise(c => { - this._pidPromiseComplete = c; + if (pid === RENDERER_NO_PROCESS_ID) { + c(undefined); + } else { + this._pidPromiseComplete = c; + } }); } @@ -56,11 +107,7 @@ export class ExtHostTerminal implements vscode.Terminal { waitOnExit?: boolean ): void { this._proxy.$createTerminal(this._name, shellPath, shellArgs, cwd, env, waitOnExit).then((id) => { - this._id = id; - this._queuedRequests.forEach((r) => { - r.run(this._proxy, this._id); - }); - this._queuedRequests = []; + this._runQueuedRequests(id); }); } @@ -87,13 +134,6 @@ export class ExtHostTerminal implements vscode.Terminal { this._queueApiRequest(this._proxy.$hide, []); } - public dispose(): void { - if (!this._disposed) { - this._disposed = true; - this._queueApiRequest(this._proxy.$dispose, []); - } - } - public _setProcessId(processId: number): void { // The event may fire 2 times when the panel is restored if (this._pidPromiseComplete) { @@ -105,34 +145,95 @@ export class ExtHostTerminal implements vscode.Terminal { public _fireOnData(data: string): void { this._onData.fire(data); } +} - private _queueApiRequest(callback: (...args: any[]) => void, args: any[]) { - let request: ApiRequest = new ApiRequest(callback, args); - if (!this._id) { - this._queuedRequests.push(request); - return; - } - request.run(this._proxy, this._id); +export class ExtHostTerminalRenderer extends BaseExtHostTerminal implements vscode.TerminalRenderer { + public get name(): string { return this._name; } + public set name(newName: string) { + this._name = newName; + this._checkDisposed(); + this._queueApiRequest(this._proxy.$terminalRendererSetName, [this._name]); } - private _checkDisposed() { - if (this._disposed) { - throw new Error('Terminal has already been disposed'); + private readonly _onInput: Emitter = new Emitter(); + public get onInput(): Event { + this._checkDisposed(); + this._queueApiRequest(this._proxy.$terminalRendererRegisterOnInputListener, [this._id]); + // Tell the main side to start sending data if it's not already + // this._proxy.$terminalRendererRegisterOnDataListener(this._id); + return this._onInput && this._onInput.event; + } + + private _dimensions: vscode.TerminalDimensions | undefined; + public get dimensions(): vscode.TerminalDimensions { return this._dimensions; } + public set dimensions(dimensions: vscode.TerminalDimensions) { + this._checkDisposed(); + this._dimensions = dimensions; + this._queueApiRequest(this._proxy.$terminalRendererSetDimensions, [dimensions]); + } + + private _maximumDimensions: vscode.TerminalDimensions; + public get maximumDimensions(): vscode.TerminalDimensions { + if (!this._maximumDimensions) { + return undefined; } + return { + rows: this._maximumDimensions.rows, + cols: this._maximumDimensions.cols + }; + } + + private readonly _onDidChangeMaximumDimensions: Emitter = new Emitter(); + public get onDidChangeMaximumDimensions(): Event { + return this._onDidChangeMaximumDimensions && this._onDidChangeMaximumDimensions.event; + } + + public get terminal(): Promise { + return this._idPromise.then(id => this._fetchTerminal(id)); + } + + constructor( + proxy: MainThreadTerminalServiceShape, + private _name: string, + private _fetchTerminal: (id: number) => Promise + ) { + super(proxy); + this._proxy.$createTerminalRenderer(this._name).then(id => { + this._runQueuedRequests(id); + }); + } + + public write(data: string): void { + this._checkDisposed(); + this._queueApiRequest(this._proxy.$terminalRendererWrite, [data]); + } + + public _fireOnInput(data: string): void { + this._onInput.fire(data); + } + + public _setMaximumDimensions(cols: number, rows: number): void { + this._maximumDimensions = { cols, rows }; + this._onDidChangeMaximumDimensions.fire(this.maximumDimensions); } } export class ExtHostTerminalService implements ExtHostTerminalServiceShape { private _proxy: MainThreadTerminalServiceShape; + private _activeTerminal: ExtHostTerminal; private _terminals: ExtHostTerminal[] = []; private _terminalProcesses: { [id: number]: cp.ChildProcess } = {}; + private _terminalRenderers: ExtHostTerminalRenderer[] = []; + public get activeTerminal(): ExtHostTerminal { return this._activeTerminal; } public get terminals(): ExtHostTerminal[] { return this._terminals; } private readonly _onDidCloseTerminal: Emitter = new Emitter(); public get onDidCloseTerminal(): Event { return this._onDidCloseTerminal && this._onDidCloseTerminal.event; } private readonly _onDidOpenTerminal: Emitter = new Emitter(); public get onDidOpenTerminal(): Event { return this._onDidOpenTerminal && this._onDidOpenTerminal.event; } + private readonly _onDidChangeActiveTerminal: Emitter = new Emitter(); + public get onDidChangeActiveTerminal(): Event { return this._onDidChangeActiveTerminal && this._onDidChangeActiveTerminal.event; } constructor( mainContext: IMainContext, @@ -143,53 +244,94 @@ export class ExtHostTerminalService implements ExtHostTerminalServiceShape { } public createTerminal(name?: string, shellPath?: string, shellArgs?: string[]): vscode.Terminal { - let terminal = new ExtHostTerminal(this._proxy, name); + const terminal = new ExtHostTerminal(this._proxy, name); terminal.create(shellPath, shellArgs); this._terminals.push(terminal); return terminal; } public createTerminalFromOptions(options: vscode.TerminalOptions): vscode.Terminal { - let terminal = new ExtHostTerminal(this._proxy, options.name); + const terminal = new ExtHostTerminal(this._proxy, options.name); terminal.create(options.shellPath, options.shellArgs, options.cwd, options.env /*, options.waitOnExit*/); this._terminals.push(terminal); return terminal; } - public $acceptTerminalProcessData(id: number, data: string): void { - let index = this._getTerminalIndexById(id); - if (index === null) { - return; + public createTerminalRenderer(name: string): vscode.TerminalRenderer { + const renderer = new ExtHostTerminalRenderer(this._proxy, name, (id) => this._getTerminalByIdEventually(id)); + this._terminalRenderers.push(renderer); + return renderer; + } + + public $acceptActiveTerminalChanged(id: number | null): void { + const original = this._activeTerminal; + if (id === null) { + this._activeTerminal = undefined; + } else { + const terminal = this._getTerminalById(id); + if (terminal) { + this._activeTerminal = terminal; + } + } + if (original !== this._activeTerminal) { + this._onDidChangeActiveTerminal.fire(this._activeTerminal); + } + } + + public $acceptTerminalProcessData(id: number, data: string): void { + const terminal = this._getTerminalById(id); + if (terminal) { + terminal._fireOnData(data); + } + } + + public $acceptTerminalRendererDimensions(id: number, cols: number, rows: number): void { + const renderer = this._getTerminalRendererById(id); + if (renderer) { + renderer._setMaximumDimensions(cols, rows); + } + } + + public $acceptTerminalRendererInput(id: number, data: string): void { + const renderer = this._getTerminalRendererById(id); + if (renderer) { + renderer._fireOnInput(data); } - const terminal = this._terminals[index]; - terminal._fireOnData(data); } public $acceptTerminalClosed(id: number): void { - let index = this._getTerminalIndexById(id); + const index = this._getTerminalObjectIndexById(this.terminals, id); if (index === null) { return; } - let terminal = this._terminals.splice(index, 1)[0]; + const terminal = this._terminals.splice(index, 1)[0]; this._onDidCloseTerminal.fire(terminal); } public $acceptTerminalOpened(id: number, name: string): void { - let index = this._getTerminalIndexById(id); + const index = this._getTerminalObjectIndexById(this._terminals, id); if (index !== null) { // The terminal has already been created (via createTerminal*), only fire the event this._onDidOpenTerminal.fire(this.terminals[index]); return; } - let terminal = new ExtHostTerminal(this._proxy, name, id); + const renderer = this._getTerminalRendererById(id); + const terminal = new ExtHostTerminal(this._proxy, name, id, renderer ? RENDERER_NO_PROCESS_ID : undefined); this._terminals.push(terminal); this._onDidOpenTerminal.fire(terminal); } public $acceptTerminalProcessId(id: number, processId: number): void { let terminal = this._getTerminalById(id); + if (terminal) { terminal._setProcessId(processId); + } else { + // Retry one more time in case the terminal has not yet been initialized. + setTimeout(() => { + terminal = this._getTerminalById(id); + terminal._setProcessId(processId); + }, EXT_HOST_CREATION_DELAY); } } @@ -232,7 +374,7 @@ export class ExtHostTerminalService implements ExtHostTerminalServiceShape { // Continue env initialization, merging in the env from the launch // config and adding keys that are needed to create the process const env = terminalEnvironment.createTerminalEnv(parentEnv, shellLaunchConfig, initialCwd, locale, cols, rows); - let cwd = Uri.parse(require.toUrl('../../parts/terminal/node')).fsPath; + const cwd = Uri.parse(require.toUrl('../../parts/terminal/node')).fsPath; const options = { env, cwd, execArgv: [] }; // Fork the process and listen for messages @@ -286,16 +428,43 @@ export class ExtHostTerminalService implements ExtHostTerminalServiceShape { this._proxy.$sendProcessExit(id, exitCode); } - private _getTerminalById(id: number): ExtHostTerminal { - let index = this._getTerminalIndexById(id); - return index !== null ? this._terminals[index] : null; + private _getTerminalByIdEventually(id: number, retries: number = 5): Promise { + return new Promise(c => { + if (retries === 0) { + c(undefined); + return; + } + + const terminal = this._getTerminalById(id); + if (terminal) { + c(terminal); + } else { + // This should only be needed immediately after createTerminalRenderer is called as + // the ExtHostTerminal has not yet been iniitalized + setTimeout(() => { + c(this._getTerminalByIdEventually(id, retries - 1)); + }, 200); + } + }); } - private _getTerminalIndexById(id: number): number { + private _getTerminalById(id: number): ExtHostTerminal { + return this._getTerminalObjectById(this._terminals, id); + } + + private _getTerminalRendererById(id: number): ExtHostTerminalRenderer { + return this._getTerminalObjectById(this._terminalRenderers, id); + } + + private _getTerminalObjectById(array: T[], id: number): T { + const index = this._getTerminalObjectIndexById(array, id); + return index !== null ? array[index] : null; + } + + private _getTerminalObjectIndexById(array: T[], id: number): number { let index: number = null; - this._terminals.some((terminal, i) => { - // TODO: This shouldn't be cas - let thisId = (terminal)._id; + array.some((item, i) => { + const thisId = item._id; if (thisId === id) { index = i; return true; diff --git a/src/vs/workbench/parts/terminal/browser/terminalFindWidget.ts b/src/vs/workbench/parts/terminal/browser/terminalFindWidget.ts index a152219e504..4369e7c078b 100644 --- a/src/vs/workbench/parts/terminal/browser/terminalFindWidget.ts +++ b/src/vs/workbench/parts/terminal/browser/terminalFindWidget.ts @@ -21,13 +21,12 @@ export class TerminalFindWidget extends SimpleFindWidget { } public find(previous: boolean) { - let val = this.inputValue; - let instance = this._terminalService.getActiveInstance(); + const instance = this._terminalService.getActiveInstance(); if (instance !== null) { if (previous) { - instance.findPrevious(val); + instance.findPrevious(this.inputValue); } else { - instance.findNext(val); + instance.findNext(this.inputValue); } } } diff --git a/src/vs/workbench/parts/terminal/browser/terminalTab.ts b/src/vs/workbench/parts/terminal/browser/terminalTab.ts index c9f9a2ab901..651f27fb815 100644 --- a/src/vs/workbench/parts/terminal/browser/terminalTab.ts +++ b/src/vs/workbench/parts/terminal/browser/terminalTab.ts @@ -278,7 +278,7 @@ export class TerminalTab extends Disposable implements ITerminalTab { // Adjust focus if the instance was active if (wasActiveInstance && this._terminalInstances.length > 0) { - let newIndex = index < this._terminalInstances.length ? index : this._terminalInstances.length - 1; + const newIndex = index < this._terminalInstances.length ? index : this._terminalInstances.length - 1; this.setActiveInstanceByIndex(newIndex); // TODO: Only focus the new instance if the tab had focus? this.activeInstance.focus(true); diff --git a/src/vs/workbench/parts/terminal/browser/terminalWidgetManager.ts b/src/vs/workbench/parts/terminal/browser/terminalWidgetManager.ts index e8d18df087e..26db47bc76e 100644 --- a/src/vs/workbench/parts/terminal/browser/terminalWidgetManager.ts +++ b/src/vs/workbench/parts/terminal/browser/terminalWidgetManager.ts @@ -7,7 +7,7 @@ import { IDisposable, dispose } from 'vs/base/common/lifecycle'; const WIDGET_HEIGHT = 29; -export class TerminalWidgetManager { +export class TerminalWidgetManager implements IDisposable { private _container: HTMLElement; private _xtermViewport: HTMLElement; @@ -24,6 +24,14 @@ export class TerminalWidgetManager { this._initTerminalHeightWatcher(terminalWrapper); } + public dispose(): void { + if (this._container) { + this._container.parentElement.removeChild(this._container); + this._container = null; + } + this._xtermViewport = null; + } + private _initTerminalHeightWatcher(terminalWrapper: HTMLElement) { // Watch the xterm.js viewport for style changes and do a layout if it changes this._xtermViewport = terminalWrapper.querySelector('.xterm-viewport'); diff --git a/src/vs/workbench/parts/terminal/common/terminal.ts b/src/vs/workbench/parts/terminal/common/terminal.ts index 6f39e8ac56e..bca473c50b7 100644 --- a/src/vs/workbench/parts/terminal/common/terminal.ts +++ b/src/vs/workbench/parts/terminal/common/terminal.ts @@ -37,6 +37,11 @@ export const IS_WORKSPACE_SHELL_ALLOWED_STORAGE_KEY = 'terminal.integrated.isWor export const NEVER_SUGGEST_SELECT_WINDOWS_SHELL_STORAGE_KEY = 'terminal.integrated.neverSuggestSelectWindowsShell'; export const NEVER_MEASURE_RENDER_TIME_STORAGE_KEY = 'terminal.integrated.neverMeasureRenderTime'; +// The creation of extension host terminals is delayed by this value (milliseconds). The purpose of +// this delay is to allow the terminal instance to initialize correctly and have its ID set before +// trying to create the corressponding object on the ext host. +export const EXT_HOST_CREATION_DELAY = 100; + export const ITerminalService = createDecorator(TERMINAL_SERVICE_ID); export const TerminalCursorStyle = { @@ -159,6 +164,12 @@ export interface IShellLaunchConfig { * of the terminal. Use \x1b over \033 or \e for the escape control character. */ initialText?: string; + + /** + * When true the terminal will be created with no process. This is primarily used to give + * extensions full control over the terminal. + */ + isRendererOnly?: boolean; } export interface ITerminalService { @@ -171,9 +182,11 @@ export interface ITerminalService { onInstanceCreated: Event; onInstanceDisposed: Event; onInstanceProcessIdReady: Event; + onInstanceDimensionsChanged: Event; onInstanceRequestExtHostProcess: Event; onInstancesChanged: Event; onInstanceTitleChanged: Event; + onActiveInstanceChanged: Event; terminalInstances: ITerminalInstance[]; terminalTabs: ITerminalTab[]; @@ -184,6 +197,12 @@ export interface ITerminalService { * default shell selection dialog may display. */ createTerminal(shell?: IShellLaunchConfig, wasNewTerminalAction?: boolean): 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. */ @@ -239,12 +258,27 @@ export interface ITerminalTab { split(terminalFocusContextKey: IContextKey, configHelper: ITerminalConfigHelper, shellLaunchConfig: IShellLaunchConfig): ITerminalInstance; } +export interface ITerminalDimensions { + /** + * The columns of the terminal. + */ + readonly cols: number; + + /** + * The rows of the terminal. + */ + readonly rows: number; +} + export interface ITerminalInstance { /** * The ID of the terminal instance, this is an arbitrary number only used to identify the * terminal instance. */ - id: number; + readonly id: number; + + readonly cols: number; + readonly rows: number; /** * The process ID of the shell process, this is undefined when there is no process associated @@ -268,6 +302,42 @@ export interface ITerminalInstance { onRequestExtHostProcess: Event; + onDimensionsChanged: Event; + + onFocus: Event; + + /** + * Attach a listener to the raw data stream coming from the pty, including ANSI escape + * sequences. + */ + onData: Event; + + /** + * 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; + + /** + * Attach a listener to listen for new lines added to this terminal instance. + * + * @param listener The listener function which takes new line strings added to the terminal, + * excluding ANSI escape sequences. The line event will fire when an LF character is added to + * the terminal (ie. the line is not wrapped). Note that this means that the line data will + * not fire for the last line, until either the line is ended with a LF character of the process + * is exited. The lineData string will contain the fully wrapped line, not containing any LF/CR + * characters. + */ + onLineData: Event; + + /** + * Attach a listener that fires when the terminal's pty process exits. The number in the event + * is the processes' exit code, an exit code of null means the process was killed as a result of + * the ITerminalInstance being disposed. + */ + onExit: Event; + processReady: TPromise; /** @@ -294,7 +364,7 @@ export interface ITerminalInstance { /** * The shell launch config used to launch the shell. */ - shellLaunchConfig: IShellLaunchConfig; + readonly shellLaunchConfig: IShellLaunchConfig; /** * Whether to disable layout for the terminal. This is useful when the size of the terminal is @@ -397,6 +467,12 @@ export interface ITerminalInstance { */ sendText(text: string, addNewLine: boolean): void; + /** + * Write text directly to the terminal, skipping the process if it exists. + * @param text The text to write. + */ + write(text: string): void; + /** Scroll the terminal buffer down 1 line. */ scrollDownLine(): void; /** Scroll the terminal buffer down 1 page. */ @@ -448,33 +524,6 @@ export interface ITerminalInstance { */ setVisible(visible: boolean): void; - /** - * Attach a listener to the raw data stream coming from the pty, including ANSI escape - * sequecnes. - * @param listener The listener function. - */ - onData(listener: (data: string) => void): IDisposable; - - /** - * Attach a listener to listen for new lines added to this terminal instance. - * - * @param listener The listener function which takes new line strings added to the terminal, - * excluding ANSI escape sequences. The line event will fire when an LF character is added to - * the terminal (ie. the line is not wrapped). Note that this means that the line data will - * not fire for the last line, until either the line is ended with a LF character of the process - * is exited. The lineData string will contain the fully wrapped line, not containing any LF/CR - * characters. - */ - onLineData(listener: (lineData: string) => void): IDisposable; - - /** - * Attach a listener that fires when the terminal's pty process exits. - * - * @param listener The listener function which takes the processes' exit code, an exit code of - * null means the process was killed as a result of the ITerminalInstance being disposed. - */ - onExit(listener: (exitCode: number) => void): IDisposable; - /** * Immediately kills the terminal's current pty process and launches a new one to replace it. * @@ -487,6 +536,8 @@ export interface ITerminalInstance { */ setTitle(title: string, eventFromProcess: boolean): void; + setDimensions(dimensions: ITerminalDimensions): void; + addDisposable(disposable: IDisposable): void; } diff --git a/src/vs/workbench/parts/terminal/common/terminalColorRegistry.ts b/src/vs/workbench/parts/terminal/common/terminalColorRegistry.ts index 0d5fb389afa..f002de9397b 100644 --- a/src/vs/workbench/parts/terminal/common/terminalColorRegistry.ts +++ b/src/vs/workbench/parts/terminal/common/terminalColorRegistry.ts @@ -160,9 +160,9 @@ const ansiColorMap = { }; export function registerColors(): void { - for (let id in ansiColorMap) { - let entry = ansiColorMap[id]; - let colorName = id.substring(13); + for (const id in ansiColorMap) { + const entry = ansiColorMap[id]; + const colorName = id.substring(13); ansiColorIdentifiers[entry.index] = registerColor(id, entry.defaults, nls.localize('terminal.ansiColor', '\'{0}\' ANSI color in the terminal.', colorName)); } } diff --git a/src/vs/workbench/parts/terminal/common/terminalService.ts b/src/vs/workbench/parts/terminal/common/terminalService.ts index f5cc15c8b42..20383cf4d66 100644 --- a/src/vs/workbench/parts/terminal/common/terminalService.ts +++ b/src/vs/workbench/parts/terminal/common/terminalService.ts @@ -41,10 +41,14 @@ export abstract class TerminalService implements ITerminalService { public get onInstanceProcessIdReady(): Event { return this._onInstanceProcessIdReady.event; } protected readonly _onInstanceRequestExtHostProcess: Emitter = new Emitter(); public get onInstanceRequestExtHostProcess(): Event { return this._onInstanceRequestExtHostProcess.event; } + protected readonly _onInstanceDimensionsChanged: Emitter = new Emitter(); + public get onInstanceDimensionsChanged(): Event { return this._onInstanceDimensionsChanged.event; } protected readonly _onInstancesChanged: Emitter = new Emitter(); public get onInstancesChanged(): Event { return this._onInstancesChanged.event; } protected readonly _onInstanceTitleChanged: Emitter = new Emitter(); public get onInstanceTitleChanged(): Event { return this._onInstanceTitleChanged.event; } + protected readonly _onActiveInstanceChanged: Emitter = new Emitter(); + public get onActiveInstanceChanged(): Event { return this._onActiveInstanceChanged.event; } protected readonly _onTabDisposed: Emitter = new Emitter(); public get onTabDisposed(): Event { return this._onTabDisposed.event; } @@ -71,6 +75,7 @@ export abstract class TerminalService implements ITerminalService { protected abstract _showTerminalCloseConfirmation(): TPromise; public abstract createTerminal(shell?: IShellLaunchConfig, wasNewTerminalAction?: boolean): ITerminalInstance; + public abstract createTerminalRenderer(name: string): ITerminalInstance; public abstract createInstance(terminalFocusContextKey: IContextKey, configHelper: ITerminalConfigHelper, container: HTMLElement, shellLaunchConfig: IShellLaunchConfig, doCreateProcess: boolean): ITerminalInstance; public abstract getActiveOrCreateInstance(wasNewTerminalAction?: boolean): ITerminalInstance; public abstract selectDefaultWindowsShell(): TPromise; @@ -152,7 +157,7 @@ export abstract class TerminalService implements ITerminalService { if (wasActiveTab && this._terminalTabs.length > 0) { // TODO: Only focus the new tab if the removed tab had focus? // const hasFocusOnExit = tab.activeInstance.hadFocusOnExit; - let newIndex = index < this._terminalTabs.length ? index : this._terminalTabs.length - 1; + const newIndex = index < this._terminalTabs.length ? index : this._terminalTabs.length - 1; this.setActiveTabByIndex(newIndex); this.getActiveInstance().focus(true); } @@ -162,6 +167,7 @@ export abstract class TerminalService implements ITerminalService { // launch. if (this._terminalTabs.length === 0 && !this._isShuttingDown) { this.hidePanel(); + this._onActiveInstanceChanged.fire(undefined); } // Fire events @@ -287,6 +293,8 @@ export abstract class TerminalService implements ITerminalService { instance.addDisposable(instance.onDisposed(this._onInstanceDisposed.fire, this._onInstanceDisposed)); instance.addDisposable(instance.onTitleChanged(this._onInstanceTitleChanged.fire, this._onInstanceTitleChanged)); instance.addDisposable(instance.onProcessIdReady(this._onInstanceProcessIdReady.fire, this._onInstanceProcessIdReady)); + instance.addDisposable(instance.onDimensionsChanged(() => this._onInstanceDimensionsChanged.fire(instance))); + instance.addDisposable(instance.onFocus(this._onActiveInstanceChanged.fire, this._onActiveInstanceChanged)); } private _getTabForInstance(instance: ITerminalInstance): ITerminalTab { @@ -301,7 +309,7 @@ export abstract class TerminalService implements ITerminalService { public showPanel(focus?: boolean): TPromise { return new TPromise((complete) => { - let panel = this._panelService.getActivePanel(); + const panel = this._panelService.getActivePanel(); if (!panel || panel.getId() !== TERMINAL_PANEL_ID) { return this._panelService.openPanel(TERMINAL_PANEL_ID, focus).then(() => { if (focus) { diff --git a/src/vs/workbench/parts/terminal/electron-browser/terminal.contribution.ts b/src/vs/workbench/parts/terminal/electron-browser/terminal.contribution.ts index 27f14953d65..6575b3d8130 100644 --- a/src/vs/workbench/parts/terminal/electron-browser/terminal.contribution.ts +++ b/src/vs/workbench/parts/terminal/electron-browser/terminal.contribution.ts @@ -66,7 +66,7 @@ registry.registerWorkbenchAction(new SyncActionDescriptor(QuickOpenTermAction, Q const actionBarRegistry = Registry.as(ActionBarExtensions.Actionbar); actionBarRegistry.registerActionBarContributor(Scope.VIEWER, QuickOpenActionTermContributor); -let configurationRegistry = Registry.as(Extensions.Configuration); +const configurationRegistry = Registry.as(Extensions.Configuration); configurationRegistry.registerConfiguration({ 'id': 'terminal', 'order': 100, @@ -368,7 +368,7 @@ registerSingleton(ITerminalService, TerminalService); // On mac cmd+` is reserved to cycle between windows, that's why the keybindings use WinCtrl const category = nls.localize('terminalCategory', "Terminal"); -let actionRegistry = Registry.as(ActionExtensions.WorkbenchActions); +const actionRegistry = Registry.as(ActionExtensions.WorkbenchActions); actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(KillTerminalAction, KillTerminalAction.ID, KillTerminalAction.LABEL), 'Terminal: Kill the Active Terminal Instance', category); actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(CopyTerminalSelectionAction, CopyTerminalSelectionAction.ID, CopyTerminalSelectionAction.LABEL, { primary: KeyMod.CtrlCmd | KeyCode.KEY_C, diff --git a/src/vs/workbench/parts/terminal/electron-browser/terminalActions.ts b/src/vs/workbench/parts/terminal/electron-browser/terminalActions.ts index 6a4b5997df5..1600743536d 100644 --- a/src/vs/workbench/parts/terminal/electron-browser/terminalActions.ts +++ b/src/vs/workbench/parts/terminal/electron-browser/terminalActions.ts @@ -71,9 +71,10 @@ export class KillTerminalAction extends Action { } public run(event?: any): TPromise { - let terminalInstance = this.terminalService.getActiveInstance(); - if (terminalInstance) { - this.terminalService.getActiveInstance().dispose(); + console.log('kill'); + const instance = this.terminalService.getActiveInstance(); + if (instance) { + instance.dispose(); if (this.terminalService.terminalInstances.length > 0) { this.terminalService.showPanel(true); } @@ -121,7 +122,7 @@ export class CopyTerminalSelectionAction extends Action { } public run(event?: any): TPromise { - let terminalInstance = this.terminalService.getActiveInstance(); + const terminalInstance = this.terminalService.getActiveInstance(); if (terminalInstance) { terminalInstance.copySelection(); } @@ -142,7 +143,7 @@ export class SelectAllTerminalAction extends Action { } public run(event?: any): TPromise { - let terminalInstance = this.terminalService.getActiveInstance(); + const terminalInstance = this.terminalService.getActiveInstance(); if (terminalInstance) { terminalInstance.selectAll(); } @@ -161,7 +162,7 @@ export abstract class BaseSendTextTerminalAction extends Action { } public run(event?: any): TPromise { - let terminalInstance = this._terminalService.getActiveInstance(); + const terminalInstance = this._terminalService.getActiveInstance(); if (terminalInstance) { terminalInstance.sendText(this._text, false); } @@ -598,7 +599,7 @@ export class RunSelectedTextInTerminalAction extends Action { if (selection.isEmpty()) { text = editor.getModel().getLineContent(selection.selectionStartLineNumber).trim(); } else { - let endOfLinePreference = os.EOL === '\n' ? EndOfLinePreference.LF : EndOfLinePreference.CRLF; + const endOfLinePreference = os.EOL === '\n' ? EndOfLinePreference.LF : EndOfLinePreference.CRLF; text = editor.getModel().getValueInRange(selection, endOfLinePreference); } instance.sendText(text, true); @@ -695,7 +696,7 @@ export class ScrollDownTerminalAction extends Action { } public run(event?: any): TPromise { - let terminalInstance = this.terminalService.getActiveInstance(); + const terminalInstance = this.terminalService.getActiveInstance(); if (terminalInstance) { terminalInstance.scrollDownLine(); } @@ -716,7 +717,7 @@ export class ScrollDownPageTerminalAction extends Action { } public run(event?: any): TPromise { - let terminalInstance = this.terminalService.getActiveInstance(); + const terminalInstance = this.terminalService.getActiveInstance(); if (terminalInstance) { terminalInstance.scrollDownPage(); } @@ -737,7 +738,7 @@ export class ScrollToBottomTerminalAction extends Action { } public run(event?: any): TPromise { - let terminalInstance = this.terminalService.getActiveInstance(); + const terminalInstance = this.terminalService.getActiveInstance(); if (terminalInstance) { terminalInstance.scrollToBottom(); } @@ -758,7 +759,7 @@ export class ScrollUpTerminalAction extends Action { } public run(event?: any): TPromise { - let terminalInstance = this.terminalService.getActiveInstance(); + const terminalInstance = this.terminalService.getActiveInstance(); if (terminalInstance) { terminalInstance.scrollUpLine(); } @@ -779,7 +780,7 @@ export class ScrollUpPageTerminalAction extends Action { } public run(event?: any): TPromise { - let terminalInstance = this.terminalService.getActiveInstance(); + const terminalInstance = this.terminalService.getActiveInstance(); if (terminalInstance) { terminalInstance.scrollUpPage(); } @@ -800,7 +801,7 @@ export class ScrollToTopTerminalAction extends Action { } public run(event?: any): TPromise { - let terminalInstance = this.terminalService.getActiveInstance(); + const terminalInstance = this.terminalService.getActiveInstance(); if (terminalInstance) { terminalInstance.scrollToTop(); } @@ -821,7 +822,7 @@ export class ClearTerminalAction extends Action { } public run(event?: any): TPromise { - let terminalInstance = this.terminalService.getActiveInstance(); + const terminalInstance = this.terminalService.getActiveInstance(); if (terminalInstance) { terminalInstance.clear(); } @@ -842,7 +843,7 @@ export class ClearSelectionTerminalAction extends Action { } public run(event?: any): TPromise { - let terminalInstance = this.terminalService.getActiveInstance(); + const terminalInstance = this.terminalService.getActiveInstance(); if (terminalInstance && terminalInstance.hasSelection()) { terminalInstance.clearSelection(); } @@ -959,7 +960,7 @@ export class QuickOpenActionTermContributor extends ActionBarContributor { } public getActions(context: any): IAction[] { - let actions: Action[] = []; + const actions: Action[] = []; if (context.element instanceof TerminalEntry) { actions.push(this.instantiationService.createInstance(RenameTerminalQuickOpenAction, RenameTerminalQuickOpenAction.ID, RenameTerminalQuickOpenAction.LABEL, context.element)); actions.push(this.instantiationService.createInstance(QuickKillTerminalAction, QuickKillTerminalAction.ID, QuickKillTerminalAction.LABEL, context.element)); diff --git a/src/vs/workbench/parts/terminal/electron-browser/terminalConfigHelper.ts b/src/vs/workbench/parts/terminal/electron-browser/terminalConfigHelper.ts index 4063c202c5f..2d887bcd678 100644 --- a/src/vs/workbench/parts/terminal/electron-browser/terminalConfigHelper.ts +++ b/src/vs/workbench/parts/terminal/electron-browser/terminalConfigHelper.ts @@ -50,12 +50,12 @@ export class TerminalConfigHelper implements ITerminalConfigHelper { public configFontIsMonospace(): boolean { this._createCharMeasureElementIfNecessary(); - let fontSize = 15; - let fontFamily = this.config.fontFamily || this._configurationService.getValue('editor').fontFamily; - let i_rect = this._getBoundingRectFor('i', fontFamily, fontSize); - let w_rect = this._getBoundingRectFor('w', fontFamily, fontSize); + const fontSize = 15; + const fontFamily = this.config.fontFamily || this._configurationService.getValue('editor').fontFamily; + const i_rect = this._getBoundingRectFor('i', fontFamily, fontSize); + const w_rect = this._getBoundingRectFor('w', fontFamily, fontSize); - let invalidBounds = !i_rect.width || !w_rect.width; + const invalidBounds = !i_rect.width || !w_rect.width; if (invalidBounds) { // There is no reason to believe the font is not Monospace. return true; @@ -88,7 +88,7 @@ export class TerminalConfigHelper implements ITerminalConfigHelper { private _measureFont(fontFamily: string, fontSize: number, letterSpacing: number, lineHeight: number): ITerminalFont { this._createCharMeasureElementIfNecessary(); - let rect = this._getBoundingRectFor('X', fontFamily, fontSize); + 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) { @@ -122,7 +122,7 @@ export class TerminalConfigHelper implements ITerminalConfigHelper { } } - let fontSize = this._toInteger(this.config.fontSize, MINIMUM_FONT_SIZE, MAXIMUM_FONT_SIZE, EDITOR_FONT_DEFAULTS.fontSize); + const fontSize = this._toInteger(this.config.fontSize, MINIMUM_FONT_SIZE, MAXIMUM_FONT_SIZE, EDITOR_FONT_DEFAULTS.fontSize); const letterSpacing = this.config.letterSpacing ? Math.max(Math.floor(this.config.letterSpacing), MINIMUM_LETTER_SPACING) : DEFAULT_LETTER_SPACING; const lineHeight = this.config.lineHeight ? Math.max(this.config.lineHeight, 1) : DEFAULT_LINE_HEIGHT; diff --git a/src/vs/workbench/parts/terminal/electron-browser/terminalInstance.ts b/src/vs/workbench/parts/terminal/electron-browser/terminalInstance.ts index bf9a5a87efb..7f7ebf03667 100644 --- a/src/vs/workbench/parts/terminal/electron-browser/terminalInstance.ts +++ b/src/vs/workbench/parts/terminal/electron-browser/terminalInstance.ts @@ -14,7 +14,7 @@ import { Terminal as XTermTerminal } from 'vscode-xterm'; import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; -import { ITerminalInstance, KEYBINDING_CONTEXT_TERMINAL_TEXT_SELECTED, TERMINAL_PANEL_ID, IShellLaunchConfig, ITerminalProcessManager, ProcessState, NEVER_MEASURE_RENDER_TIME_STORAGE_KEY } from 'vs/workbench/parts/terminal/common/terminal'; +import { ITerminalInstance, KEYBINDING_CONTEXT_TERMINAL_TEXT_SELECTED, TERMINAL_PANEL_ID, IShellLaunchConfig, ITerminalProcessManager, ProcessState, NEVER_MEASURE_RENDER_TIME_STORAGE_KEY, ITerminalDimensions } from 'vs/workbench/parts/terminal/common/terminal'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { TabFocus } from 'vs/editor/common/config/commonEditorConfig'; @@ -63,8 +63,8 @@ export class TerminalInstance implements ITerminalInstance { private _terminalHasTextContextKey: IContextKey; private _cols: number; private _rows: number; + private _dimensionsOverride: ITerminalDimensions; private _windowsShellHelper: WindowsShellHelper; - private _onLineDataListeners: ((lineData: string) => void)[]; private _xtermReadyPromise: TPromise; private _disposables: lifecycle.IDisposable[]; @@ -76,6 +76,8 @@ export class TerminalInstance implements ITerminalInstance { public disableLayout: boolean; public get id(): number { return this._id; } + public get cols(): number { return this._cols; } + public get rows(): number { return this._rows; } // TODO: Ideally processId would be merged into processReady public get processId(): number | undefined { return this._processManager ? this._processManager.shellProcessId : undefined; } // TODO: How does this work with detached processes? @@ -84,10 +86,11 @@ export class TerminalInstance implements ITerminalInstance { 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 Object.freeze(this._shellLaunchConfig); } + public get shellLaunchConfig(): IShellLaunchConfig { return this._shellLaunchConfig; } public get commandTracker(): TerminalCommandTracker { return this._commandTracker; } - + private readonly _onExit: Emitter = new Emitter(); + public get onExit(): Event { return this._onExit.event; } private readonly _onDisposed: Emitter = new Emitter(); public get onDisposed(): Event { return this._onDisposed.event; } private readonly _onFocused: Emitter = new Emitter(); @@ -96,15 +99,24 @@ export class TerminalInstance implements ITerminalInstance { public get onProcessIdReady(): Event { return this._onProcessIdReady.event; } private readonly _onTitleChanged: Emitter = new Emitter(); public get onTitleChanged(): Event { return this._onTitleChanged.event; } + private readonly _onData: Emitter = new Emitter(); + public get onData(): Event { return this._onData.event; } + private readonly _onLineData: Emitter = new Emitter(); + public get onLineData(): Event { return this._onLineData.event; } + private readonly _onRendererInput: Emitter = new Emitter(); + public get onRendererInput(): Event { return this._onRendererInput.event; } private readonly _onRequestExtHostProcess: Emitter = new Emitter(); public get onRequestExtHostProcess(): Event { return this._onRequestExtHostProcess.event; } + private readonly _onDimensionsChanged: Emitter = new Emitter(); + public get onDimensionsChanged(): Event { return this._onDimensionsChanged.event; } + private readonly _onFocus: Emitter = new Emitter(); + public get onFocus(): Event { return this._onFocus.event; } public constructor( - private _terminalFocusContextKey: IContextKey, - private _configHelper: TerminalConfigHelper, + private readonly _terminalFocusContextKey: IContextKey, + private readonly _configHelper: TerminalConfigHelper, private _container: HTMLElement, private _shellLaunchConfig: IShellLaunchConfig, - doCreateProcess: boolean, @IContextKeyService private readonly _contextKeyService: IContextKeyService, @IKeybindingService private readonly _keybindingService: IKeybindingService, @INotificationService private readonly _notificationService: INotificationService, @@ -118,7 +130,6 @@ export class TerminalInstance implements ITerminalInstance { ) { this._disposables = []; this._skipTerminalCommands = []; - this._onLineDataListeners = []; this._isExiting = false; this._hadFocusOnExit = false; this._isVisible = false; @@ -130,8 +141,10 @@ export class TerminalInstance implements ITerminalInstance { this._logService.trace(`terminalInstance#ctor (id: ${this.id})`, this._shellLaunchConfig); this._initDimensions(); - if (doCreateProcess) { + if (!this.shellLaunchConfig.isRendererOnly) { this._createProcess(); + } else { + this.setTitle(this._shellLaunchConfig.name, false); } this._xtermReadyPromise = this._createXterm(); @@ -142,14 +155,14 @@ export class TerminalInstance implements ITerminalInstance { } }); - this._configurationService.onDidChangeConfiguration(e => { + this.addDisposable(this._configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration('terminal.integrated')) { this.updateConfig(); } if (e.affectsConfiguration('editor.accessibilitySupport')) { this.updateAccessibilitySupport(); } - }); + })); } public addDisposable(disposable: lifecycle.IDisposable): void { @@ -286,6 +299,13 @@ export class TerminalInstance implements ITerminalInstance { // TODO: How does the cwd work on detached processes? this._linkHandler = this._instantiationService.createInstance(TerminalLinkHandler, this._xterm, platform.platform, this._processManager.initialCwd); } + this._xterm.on('focus', () => this._onFocus.fire(this)); + + // Register listener to trigger the onInput ext API if the terminal is a renderer only + if (this._shellLaunchConfig.isRendererOnly) { + this._xterm.on('data', (data) => this._sendRendererInput(data)); + } + this._commandTracker = new TerminalCommandTracker(this._xterm); this._disposables.push(this._themeService.onThemeChange(theme => this._updateTheme(theme))); } @@ -439,7 +459,7 @@ export class TerminalInstance implements ITerminalInstance { } private _measureRenderTime(): void { - let frameTimes: number[] = []; + const frameTimes: number[] = []; const textRenderLayer = (this._xterm).renderer._renderLayers[0]; const originalOnGridChanged = textRenderLayer.onGridChanged; @@ -538,18 +558,21 @@ export class TerminalInstance implements ITerminalInstance { public dispose(): void { this._logService.trace(`terminalInstance#dispose (id: ${this.id})`); - if (this._windowsShellHelper) { - this._windowsShellHelper.dispose(); - } - if (this._linkHandler) { - this._linkHandler.dispose(); - } + this._windowsShellHelper = lifecycle.dispose(this._windowsShellHelper); + this._linkHandler = lifecycle.dispose(this._linkHandler); + this._commandTracker = lifecycle.dispose(this._commandTracker); + this._widgetManager = lifecycle.dispose(this._widgetManager); + if (this._xterm && this._xterm.element) { this._hadFocusOnExit = dom.hasClass(this._xterm.element, 'focus'); } if (this._wrapperElement) { + if ((this._wrapperElement).xterm) { + (this._wrapperElement).xterm = null; + } this._container.removeChild(this._wrapperElement); this._wrapperElement = null; + this._xtermElement = null; } if (this._xterm) { const buffer = (this._xterm.buffer); @@ -557,9 +580,7 @@ export class TerminalInstance implements ITerminalInstance { this._xterm.dispose(); this._xterm = null; } - if (this._processManager) { - this._processManager.dispose(); - } + this._processManager = lifecycle.dispose(this._processManager); if (!this._isDisposed) { this._isDisposed = true; this._onDisposed.fire(this); @@ -584,17 +605,39 @@ export class TerminalInstance implements ITerminalInstance { document.execCommand('paste'); } - public sendText(text: string, addNewLine: boolean): void { - this._processManager.ptyProcessReady.then(() => { - // Normalize line endings to 'enter' press. - text = text.replace(TerminalInstance.EOL_REGEX, '\r'); - if (addNewLine && text.substr(text.length - 1) !== '\r') { - text += '\r'; + public write(text: string): void { + this._xtermReadyPromise.then(() => { + if (!this._xterm) { + return; + } + this._xterm.write(text); + if (this._shellLaunchConfig.isRendererOnly) { + // Fire onData API in the extension host + this._onData.fire(text); } - this._processManager.write(text); }); } + public sendText(text: string, addNewLine: boolean): void { + // Normalize line endings to 'enter' press. + text = text.replace(TerminalInstance.EOL_REGEX, '\r'); + if (addNewLine && text.substr(text.length - 1) !== '\r') { + 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); + }); + } + } + } + public setVisible(visible: boolean): void { this._isVisible = visible; if (this._wrapperElement) { @@ -660,6 +703,8 @@ export class TerminalInstance implements ITerminalInstance { this._processManager.onProcessExit(exitCode => this._onProcessExit(exitCode)); this._processManager.createProcess(this._shellLaunchConfig, this._cols, this._rows); + this._processManager.onProcessData(data => this._onData.fire(data)); + if (this._shellLaunchConfig.name) { this.setTitle(this._shellLaunchConfig.name, false); } else { @@ -751,6 +796,8 @@ export class TerminalInstance implements ITerminalInstance { } } } + + this._onExit.fire(exitCode); } private _attachPressAnyKeyToCloseListener() { @@ -791,26 +838,16 @@ export class TerminalInstance implements ITerminalInstance { this._shellLaunchConfig = shell; } - public onData(listener: (data: string) => void): lifecycle.IDisposable { - return this._processManager.onProcessData(data => listener(data)); - } + private _sendRendererInput(input: string): void { + if (this._processManager) { + throw new Error('onRendererInput attempted to be used on a regular terminal'); + } - public onLineData(listener: (lineData: string) => void): lifecycle.IDisposable { - this._onLineDataListeners.push(listener); - return { - dispose: () => { - const i = this._onLineDataListeners.indexOf(listener); - if (i >= 0) { - this._onLineDataListeners.splice(i, 1); - } - } - }; + // For terminal renderers onData fires on keystrokes and when sendText is called. + this._onRendererInput.fire(input); } private _onLineFeed(): void { - if (this._onLineDataListeners.length === 0) { - return; - } const buffer = (this._xterm.buffer); const newLine = buffer.lines.get(buffer.ybase + buffer.y); if (!newLine.isWrapped) { @@ -823,17 +860,7 @@ export class TerminalInstance implements ITerminalInstance { while (lineIndex >= 0 && buffer.lines.get(lineIndex--).isWrapped) { lineData = buffer.translateBufferLineToString(lineIndex, false) + lineData; } - this._onLineDataListeners.forEach(listener => { - try { - listener(lineData); - } catch (err) { - console.error(`onLineData listener threw`, err); - } - }); - } - - public onExit(listener: (exitCode: number) => void): lifecycle.IDisposable { - return this._processManager.onProcessExit(listener); + this._onLineData.fire(lineData); } public updateConfig(): void { @@ -905,6 +932,21 @@ export class TerminalInstance implements ITerminalInstance { return; } + if (this._xterm) { + this._xterm.element.style.width = terminalWidth + 'px'; + } + + this._resize(); + } + + private _resize(): void { + let cols = this._cols; + let rows = this._rows; + if (this._dimensionsOverride && this._dimensionsOverride.cols && this._dimensionsOverride.rows) { + cols = Math.min(Math.max(this._dimensionsOverride.cols, 2), this._cols); + rows = Math.min(Math.max(this._dimensionsOverride.rows, 2), this._rows); + } + if (this._xterm) { const font = this._configHelper.getFont(this._xterm); @@ -921,8 +963,11 @@ export class TerminalInstance implements ITerminalInstance { this._safeSetOption('drawBoldTextInBrightColors', config.drawBoldTextInBrightColors); } - this._xterm.resize(this._cols, this._rows); - this._xterm.element.style.width = terminalWidth + 'px'; + if (cols !== this._xterm.getOption('cols') || rows !== this._xterm.getOption('rows')) { + this._onDimensionsChanged.fire(); + } + + this._xterm.resize(cols, rows); if (this._isVisible) { // Force the renderer to unpause by simulating an IntersectionObserver event. This // is to fix an issue where dragging the window to the top of the screen to maximize @@ -935,7 +980,7 @@ export class TerminalInstance implements ITerminalInstance { } if (this._processManager) { - this._processManager.ptyProcessReady.then(() => this._processManager.setDimensions(this._cols, this._rows)); + this._processManager.ptyProcessReady.then(() => this._processManager.setDimensions(cols, rows)); } } @@ -964,6 +1009,11 @@ export class TerminalInstance implements ITerminalInstance { } } + public setDimensions(dimensions: ITerminalDimensions): void { + this._dimensionsOverride = dimensions; + this._resize(); + } + private _getXtermTheme(theme?: ITheme): any { if (!theme) { theme = this._themeService.getTheme(); diff --git a/src/vs/workbench/parts/terminal/electron-browser/terminalLinkHandler.ts b/src/vs/workbench/parts/terminal/electron-browser/terminalLinkHandler.ts index d19528a9e31..7179711f900 100644 --- a/src/vs/workbench/parts/terminal/electron-browser/terminalLinkHandler.ts +++ b/src/vs/workbench/parts/terminal/electron-browser/terminalLinkHandler.ts @@ -121,6 +121,7 @@ export class TerminalLinkHandler { } public dispose(): void { + this._xterm = null; this._hoverDisposables = dispose(this._hoverDisposables); this._mouseMoveDisposable = dispose(this._mouseMoveDisposable); } @@ -172,7 +173,7 @@ export class TerminalLinkHandler { } private _handleHypertextLink(url: string): void { - let uri = Uri.parse(url); + const uri = Uri.parse(url); this._openerService.open(uri); } diff --git a/src/vs/workbench/parts/terminal/electron-browser/terminalPanel.ts b/src/vs/workbench/parts/terminal/electron-browser/terminalPanel.ts index 58d1db42fbf..671a3342519 100644 --- a/src/vs/workbench/parts/terminal/electron-browser/terminalPanel.ts +++ b/src/vs/workbench/parts/terminal/electron-browser/terminalPanel.ts @@ -77,7 +77,7 @@ export class TerminalPanel extends Panel { } if (e.affectsConfiguration('terminal.integrated.fontFamily') || e.affectsConfiguration('editor.fontFamily')) { - let configHelper = this._terminalService.configHelper; + const configHelper = this._terminalService.configHelper; if (configHelper instanceof TerminalConfigHelper) { if (!configHelper.configFontIsMonospace()) { const choices: IPromptChoice[] = [{ @@ -203,7 +203,7 @@ export class TerminalPanel extends Panel { this._terminalService.getActiveInstance().focus(); } else if (event.which === 3) { if (this._terminalService.configHelper.config.rightClickBehavior === 'copyPaste') { - let terminal = this._terminalService.getActiveInstance(); + const terminal = this._terminalService.getActiveInstance(); if (terminal.hasSelection()) { terminal.copySelection(); terminal.clearSelection(); @@ -230,7 +230,7 @@ export class TerminalPanel extends Panel { } if (event.which === 1) { - let terminal = this._terminalService.getActiveInstance(); + const terminal = this._terminalService.getActiveInstance(); if (terminal.hasSelection()) { terminal.copySelection(); } @@ -240,7 +240,7 @@ export class TerminalPanel extends Panel { this._register(dom.addDisposableListener(this._parentDomElement, 'contextmenu', (event: MouseEvent) => { if (!this._cancelContextMenu) { const standardEvent = new StandardMouseEvent(event); - let anchor: { x: number, y: number } = { x: standardEvent.posx, y: standardEvent.posy }; + const anchor: { x: number, y: number } = { x: standardEvent.posx, y: standardEvent.posy }; this._contextMenuService.showContextMenu({ getAnchor: () => anchor, getActions: () => TPromise.as(this._getContextMenuActions()), @@ -269,7 +269,7 @@ export class TerminalPanel extends Panel { // Check if files were dragged from the tree explorer let path: string; - let resources = e.dataTransfer.getData(DataTransfers.RESOURCES); + const resources = e.dataTransfer.getData(DataTransfers.RESOURCES); if (resources) { path = URI.parse(JSON.parse(resources)[0]).path; } else if (e.dataTransfer.files.length > 0) { @@ -315,15 +315,15 @@ registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => { } // Borrow the editor's hover background for now - let hoverBackground = theme.getColor(editorHoverBackground); + const hoverBackground = theme.getColor(editorHoverBackground); if (hoverBackground) { collector.addRule(`.monaco-workbench .panel.integrated-terminal .terminal-message-widget { background-color: ${hoverBackground}; }`); } - let hoverBorder = theme.getColor(editorHoverBorder); + const hoverBorder = theme.getColor(editorHoverBorder); if (hoverBorder) { collector.addRule(`.monaco-workbench .panel.integrated-terminal .terminal-message-widget { border: 1px solid ${hoverBorder}; }`); } - let hoverForeground = theme.getColor(editorForeground); + const hoverForeground = theme.getColor(editorForeground); if (hoverForeground) { collector.addRule(`.monaco-workbench .panel.integrated-terminal .terminal-message-widget { color: ${hoverForeground}; }`); } diff --git a/src/vs/workbench/parts/terminal/electron-browser/terminalService.ts b/src/vs/workbench/parts/terminal/electron-browser/terminalService.ts index a83995ab5f5..b7fd6898f18 100644 --- a/src/vs/workbench/parts/terminal/electron-browser/terminalService.ts +++ b/src/vs/workbench/parts/terminal/electron-browser/terminalService.ts @@ -91,8 +91,12 @@ export class TerminalService extends AbstractTerminalService implements ITermina return instance; } + public createTerminalRenderer(name: string): ITerminalInstance { + return this.createTerminal({ name, isRendererOnly: true }); + } + public createInstance(terminalFocusContextKey: IContextKey, configHelper: ITerminalConfigHelper, container: HTMLElement, shellLaunchConfig: IShellLaunchConfig, doCreateProcess: boolean): ITerminalInstance { - const instance = this._instantiationService.createInstance(TerminalInstance, terminalFocusContextKey, configHelper, container, shellLaunchConfig, true); + const instance = this._instantiationService.createInstance(TerminalInstance, terminalFocusContextKey, configHelper, container, shellLaunchConfig); this._onInstanceCreated.fire(instance); return instance; } @@ -109,7 +113,7 @@ export class TerminalService extends AbstractTerminalService implements ITermina public focusFindWidget(): TPromise { return this.showPanel(false).then(() => { - let panel = this._panelService.getActivePanel() as TerminalPanel; + const panel = this._panelService.getActivePanel() as TerminalPanel; panel.focusFindWidget(); this._findWidgetVisible.set(true); }); diff --git a/src/vs/workbench/parts/terminal/node/terminalCommandTracker.ts b/src/vs/workbench/parts/terminal/node/terminalCommandTracker.ts index d11af07bd79..bdde9f6e0b9 100644 --- a/src/vs/workbench/parts/terminal/node/terminalCommandTracker.ts +++ b/src/vs/workbench/parts/terminal/node/terminalCommandTracker.ts @@ -5,6 +5,7 @@ import { Terminal, IMarker } from 'vscode-xterm'; import { ITerminalCommandTracker } from 'vs/workbench/parts/terminal/common/terminal'; +import { IDisposable } from 'vs/base/common/lifecycle'; /** * The minimize size of the prompt in which to assume the line is a command. @@ -21,7 +22,7 @@ export enum ScrollPosition { Middle } -export class TerminalCommandTracker implements ITerminalCommandTracker { +export class TerminalCommandTracker implements ITerminalCommandTracker, IDisposable { private _currentMarker: IMarker | Boundary = Boundary.Bottom; private _selectionStart: IMarker | Boundary | null = null; private _isDisposable: boolean = false; @@ -32,6 +33,10 @@ export class TerminalCommandTracker implements ITerminalCommandTracker { this._xterm.on('key', key => this._onKey(key)); } + public dispose(): void { + this._xterm = null; + } + private _onKey(key: string): void { if (key === '\x0d') { this._onEnter(); @@ -194,7 +199,7 @@ export class TerminalCommandTracker implements ITerminalCommandTracker { if (this._currentMarker === Boundary.Bottom) { this._currentMarker = this._xterm.addMarker(this._getOffset() - 1); } else { - let offset = this._getOffset(); + const offset = this._getOffset(); if (this._isDisposable) { this._currentMarker.dispose(); } @@ -217,7 +222,7 @@ export class TerminalCommandTracker implements ITerminalCommandTracker { if (this._currentMarker === Boundary.Top) { this._currentMarker = this._xterm.addMarker(this._getOffset() + 1); } else { - let offset = this._getOffset(); + const offset = this._getOffset(); if (this._isDisposable) { this._currentMarker.dispose(); } diff --git a/src/vs/workbench/parts/terminal/node/terminalEnvironment.ts b/src/vs/workbench/parts/terminal/node/terminalEnvironment.ts index 2c60754603d..8e2bca6dadd 100644 --- a/src/vs/workbench/parts/terminal/node/terminalEnvironment.ts +++ b/src/vs/workbench/parts/terminal/node/terminalEnvironment.ts @@ -25,9 +25,9 @@ export function mergeEnvironments(parent: IStringDictionary, other: IStr // On Windows apply the new values ignoring case, while still retaining // the case of the original key. if (platform.isWindows) { - for (let configKey in other) { + for (const configKey in other) { let actualKey = configKey; - for (let envKey in parent) { + for (const envKey in parent) { if (configKey.toLowerCase() === envKey.toLowerCase()) { actualKey = envKey; break; diff --git a/src/vs/workbench/parts/terminal/node/terminalProcess.ts b/src/vs/workbench/parts/terminal/node/terminalProcess.ts index 2ad362e73d2..e9126845545 100644 --- a/src/vs/workbench/parts/terminal/node/terminalProcess.ts +++ b/src/vs/workbench/parts/terminal/node/terminalProcess.ts @@ -9,7 +9,7 @@ import * as pty from 'node-pty'; // The pty process needs to be run in its own child process to get around maxing out CPU on Mac, // see https://github.com/electron/electron/issues/38 -var shellName: string; +let shellName: string; if (os.platform() === 'win32') { shellName = path.basename(process.env.PTYSHELL); } else { @@ -17,12 +17,12 @@ if (os.platform() === 'win32') { // color prompt as defined in the default ~/.bashrc file. shellName = 'xterm-256color'; } -var shell = process.env.PTYSHELL; -var args = getArgs(); -var cwd = process.env.PTYCWD; -var cols = process.env.PTYCOLS; -var rows = process.env.PTYROWS; -var currentTitle = ''; +const shell = process.env.PTYSHELL; +const args = getArgs(); +const cwd = process.env.PTYCWD; +const cols = process.env.PTYCOLS; +const rows = process.env.PTYROWS; +let currentTitle = ''; setupPlanB(Number(process.env.PTYPID)); cleanEnv(); @@ -34,7 +34,7 @@ interface IOptions { rows?: number; } -var options: IOptions = { +const options: IOptions = { name: shellName, cwd }; @@ -43,10 +43,10 @@ if (cols && rows) { options.rows = parseInt(rows, 10); } -var ptyProcess = pty.spawn(shell, args, options); +const ptyProcess = pty.spawn(shell, args, options); -var closeTimeout: number; -var exitCode: number; +let closeTimeout: number; +let exitCode: number; // Allow any trailing data events to be sent before the exit event is sent. // See https://github.com/Tyriar/node-pty/issues/72 @@ -95,8 +95,8 @@ function getArgs(): string | string[] { if (process.env['PTYSHELLCMDLINE']) { return process.env['PTYSHELLCMDLINE']; } - var args = []; - var i = 0; + const args = []; + let i = 0; while (process.env['PTYSHELLARG' + i]) { args.push(process.env['PTYSHELLARG' + i]); i++; @@ -105,7 +105,7 @@ function getArgs(): string | string[] { } function cleanEnv() { - var keys = [ + const keys = [ 'AMD_ENTRYPOINT', 'ELECTRON_NO_ASAR', 'ELECTRON_RUN_AS_NODE', @@ -124,7 +124,7 @@ function cleanEnv() { delete process.env[key]; } }); - var i = 0; + let i = 0; while (process.env['PTYSHELLARG' + i]) { delete process.env['PTYSHELLARG' + i]; i++;