diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index d2580a04439..f5184cce2fa 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -349,6 +349,25 @@ declare module 'vscode' { onData: Event; } + // A TerminalRender does not own a process, it's similar to an output window + // but it understands ANSI + export interface TerminalRenderer { + // Extensions can set the name (what appears in the dropdown) + name: string; + + // Setting to undefined will reset to use the maximum available + // dimensions: TerminalDimensions; + + // Write to xterm.js + write(data: string): void; // out + + // key press, or sendText was triggered by an extension on the terminal + onData: Event; // in + + // Fires when the panel area is resized, this DOES NOT fire when `dimensions` is set + // onDidChangeDimensions: Event; + } + export namespace window { /** * The currently opened terminals or an empty array. @@ -362,6 +381,8 @@ declare module 'vscode' { * [createTerminal](#window.createTerminal) API or commands. */ export const onDidOpenTerminal: Event; + + 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..b890af86f4a 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadTerminalService.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadTerminalService.ts @@ -60,6 +60,10 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape return TPromise.as(this.terminalService.createTerminal(shellLaunchConfig).id); } + public $createTerminalRenderer(name: string): TPromise { + return TPromise.as(this.terminalService.createTerminalRenderer(name).id); + } + public $show(terminalId: number, preserveFocus: boolean): void { let terminalInstance = this.terminalService.getInstanceFromId(terminalId); if (terminalInstance) { @@ -81,6 +85,13 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape } } + public $write(terminalId: number, text: string): void { + let terminalInstance = this.terminalService.getInstanceFromId(terminalId); + if (terminalInstance && terminalInstance.shellLaunchConfig.isRendererOnly) { + terminalInstance.write(text); + } + } + public $sendText(terminalId: number, text: string, addNewLine: boolean): void { let terminalInstance = this.terminalService.getInstanceFromId(terminalId); if (terminalInstance) { diff --git a/src/vs/workbench/api/node/extHost.api.impl.ts b/src/vs/workbench/api/node/extHost.api.impl.ts index cc5ebbc10fc..85e2a873c23 100644 --- a/src/vs/workbench/api/node/extHost.api.impl.ts +++ b/src/vs/workbench/api/node/extHost.api.impl.ts @@ -425,6 +425,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 8da4c2552f5..18ae4190b20 100644 --- a/src/vs/workbench/api/node/extHost.protocol.ts +++ b/src/vs/workbench/api/node/extHost.protocol.ts @@ -316,9 +316,11 @@ 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; + $write(terminalId: number, text: string): void; $show(terminalId: number, preserveFocus: boolean): void; $registerOnDataListener(terminalId: number): void; diff --git a/src/vs/workbench/api/node/extHostTerminalService.ts b/src/vs/workbench/api/node/extHostTerminalService.ts index c4f2e996461..00b91e36a34 100644 --- a/src/vs/workbench/api/node/extHostTerminalService.ts +++ b/src/vs/workbench/api/node/extHostTerminalService.ts @@ -17,9 +17,7 @@ import { ExtHostConfiguration } from 'vs/workbench/api/node/extHostConfiguration import { ILogService } from 'vs/platform/log/common/log'; export class ExtHostTerminal implements vscode.Terminal { - private _name: string; private _id: number; - private _proxy: MainThreadTerminalServiceShape; private _disposed: boolean; private _queuedRequests: ApiRequest[]; private _pidPromise: Promise; @@ -33,12 +31,10 @@ export class ExtHostTerminal implements vscode.Terminal { } constructor( - proxy: MainThreadTerminalServiceShape, - name: string = '', + private _proxy: MainThreadTerminalServiceShape, + private _name: string, id?: number ) { - this._proxy = proxy; - this._name = name; if (id) { this._id = id; } @@ -122,6 +118,50 @@ export class ExtHostTerminal implements vscode.Terminal { } } +export class ExtHostTerminalRenderer implements vscode.TerminalRenderer { + private _id: number; + // private _disposed: boolean; + private _queuedRequests: ApiRequest[]; + + public get name(): string { return this._name; } + + private readonly _onData: Emitter = new Emitter(); + public get onData(): Event { return this._onData && this._onData.event; } + + constructor( + private _proxy: MainThreadTerminalServiceShape, + private _name: string + ) { + this._proxy.$createTerminalRenderer(this._name).then((id) => { + this._id = id; + this._queuedRequests.forEach((r) => { + r.run(this._proxy, this._id); + }); + this._queuedRequests = []; + }); + } + + public write(data: string): void { + this._checkDisposed(); + this._queueApiRequest(this._proxy.$write, []); + } + + 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); + } + + private _checkDisposed() { + if (this._disposed) { + throw new Error('Terminal has already been disposed'); + } + } +} + export class ExtHostTerminalService implements ExtHostTerminalServiceShape { private _proxy: MainThreadTerminalServiceShape; private _terminals: ExtHostTerminal[] = []; @@ -143,21 +183,27 @@ 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 createTerminalRenderer(name: string): vscode.TerminalRenderer { + const terminalRenderer = new ExtHostTerminalRenderer(this._proxy, name); + console.log('Creating new terminal renderer'); + return terminalRenderer; + } + public $acceptTerminalProcessData(id: number, data: string): void { - let index = this._getTerminalIndexById(id); + const index = this._getTerminalIndexById(id); if (index === null) { return; } @@ -166,28 +212,28 @@ export class ExtHostTerminalService implements ExtHostTerminalServiceShape { } public $acceptTerminalClosed(id: number): void { - let index = this._getTerminalIndexById(id); + const index = this._getTerminalIndexById(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._getTerminalIndexById(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 terminal = new ExtHostTerminal(this._proxy, name, id); this._terminals.push(terminal); this._onDidOpenTerminal.fire(terminal); } public $acceptTerminalProcessId(id: number, processId: number): void { - let terminal = this._getTerminalById(id); + const terminal = this._getTerminalById(id); if (terminal) { terminal._setProcessId(processId); } diff --git a/src/vs/workbench/parts/terminal/common/terminal.ts b/src/vs/workbench/parts/terminal/common/terminal.ts index 4ca2847cbf8..f8b361e9ede 100644 --- a/src/vs/workbench/parts/terminal/common/terminal.ts +++ b/src/vs/workbench/parts/terminal/common/terminal.ts @@ -156,6 +156,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 { @@ -181,6 +187,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. */ @@ -396,6 +408,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. */ diff --git a/src/vs/workbench/parts/terminal/common/terminalService.ts b/src/vs/workbench/parts/terminal/common/terminalService.ts index e294388516b..f94b09b4bef 100644 --- a/src/vs/workbench/parts/terminal/common/terminalService.ts +++ b/src/vs/workbench/parts/terminal/common/terminalService.ts @@ -71,6 +71,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; diff --git a/src/vs/workbench/parts/terminal/electron-browser/terminalInstance.ts b/src/vs/workbench/parts/terminal/electron-browser/terminalInstance.ts index a0ffa0e0c21..dcb4f4818ec 100644 --- a/src/vs/workbench/parts/terminal/electron-browser/terminalInstance.ts +++ b/src/vs/workbench/parts/terminal/electron-browser/terminalInstance.ts @@ -101,11 +101,10 @@ export class TerminalInstance implements ITerminalInstance { public get onRequestExtHostProcess(): Event { return this._onRequestExtHostProcess.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, @@ -114,7 +113,7 @@ export class TerminalInstance implements ITerminalInstance { @IClipboardService private readonly _clipboardService: IClipboardService, @IThemeService private readonly _themeService: IThemeService, @IConfigurationService private readonly _configurationService: IConfigurationService, - @ILogService private _logService: ILogService, + @ILogService private readonly _logService: ILogService, @IStorageService private readonly _storageService: IStorageService, ) { this._disposables = []; @@ -132,7 +131,7 @@ 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(); } @@ -582,6 +581,15 @@ export class TerminalInstance implements ITerminalInstance { document.execCommand('paste'); } + public write(text: string): void { + this._xtermReadyPromise.then(() => { + if (!this._xterm) { + return; + } + this._xterm.write(text); + }); + } + public sendText(text: string, addNewLine: boolean): void { this._processManager.ptyProcessReady.then(() => { // Normalize line endings to 'enter' press. diff --git a/src/vs/workbench/parts/terminal/electron-browser/terminalService.ts b/src/vs/workbench/parts/terminal/electron-browser/terminalService.ts index d6279436f55..7d339640f60 100644 --- a/src/vs/workbench/parts/terminal/electron-browser/terminalService.ts +++ b/src/vs/workbench/parts/terminal/electron-browser/terminalService.ts @@ -90,8 +90,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; }