diff --git a/packages/plugin-ext/src/api/plugin-api.ts b/packages/plugin-ext/src/api/plugin-api.ts index 4ede069b7b81d..e04a06bc482f4 100644 --- a/packages/plugin-ext/src/api/plugin-api.ts +++ b/packages/plugin-ext/src/api/plugin-api.ts @@ -161,7 +161,11 @@ export interface CommandRegistryExt { } export interface TerminalServiceExt { - $terminalClosed(id: number): void; + $terminalCreated(id: string, name: string): void; + $terminalNameChanged(id: string, name: string): void; + $terminalOpened(id: string, processId: number): void; + $terminalClosed(id: string): void; + $currentTerminalChanged(id: string | undefined): void; } export interface ConnectionMain { @@ -183,7 +187,7 @@ export interface TerminalServiceMain { * Create new Terminal with Terminal options. * @param options - object with parameters to create new terminal. */ - $createTerminal(options: theia.TerminalOptions): PromiseLike; + $createTerminal(id: string, options: theia.TerminalOptions): Promise; /** * Send text to the terminal by id. @@ -191,26 +195,26 @@ export interface TerminalServiceMain { * @param text - text content. * @param addNewLine - in case true - add new line after the text, otherwise - don't apply new line. */ - $sendText(id: number, text: string, addNewLine?: boolean): void; + $sendText(id: string, text: string, addNewLine?: boolean): void; /** * Show terminal on the UI panel. * @param id - terminal id. * @param preserveFocus - set terminal focus in case true value, and don't set focus otherwise. */ - $show(id: number, preserveFocus?: boolean): void; + $show(id: string, preserveFocus?: boolean): void; /** * Hide UI panel where is located terminal widget. * @param id - terminal id. */ - $hide(id: number): void; + $hide(id: string): void; /** * Distroy terminal. * @param id - terminal id. */ - $dispose(id: number): void; + $dispose(id: string): void; } export interface AutoFocus { diff --git a/packages/plugin-ext/src/main/browser/terminal-main.ts b/packages/plugin-ext/src/main/browser/terminal-main.ts index 5874a6a0d50fb..62927a5d9ece1 100644 --- a/packages/plugin-ext/src/main/browser/terminal-main.ts +++ b/packages/plugin-ext/src/main/browser/terminal-main.ts @@ -14,93 +14,114 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { TerminalOptions } from '@theia/plugin'; -import { TerminalServiceMain, TerminalServiceExt, MAIN_RPC_CONTEXT } from '../../api/plugin-api'; import { interfaces } from 'inversify'; +import { ApplicationShell, WidgetOpenerOptions } from '@theia/core/lib/browser'; +import { TerminalOptions } from '@theia/plugin'; +import { TerminalWidget } from '@theia/terminal/lib/browser/base/terminal-widget'; import { TerminalService } from '@theia/terminal/lib/browser/base/terminal-service'; -import { TerminalWidget, TerminalWidgetOptions } from '@theia/terminal/lib/browser/base/terminal-widget'; +import { TerminalServiceMain, TerminalServiceExt, MAIN_RPC_CONTEXT } from '../../api/plugin-api'; import { RPCProtocol } from '../../api/rpc-protocol'; -import { ApplicationShell } from '@theia/core/lib/browser'; /** * Plugin api service allows working with terminal emulator. */ export class TerminalServiceMainImpl implements TerminalServiceMain { - private readonly terminalService: TerminalService; + private readonly terminals: TerminalService; private readonly shell: ApplicationShell; - protected readonly terminals = new Map(); private readonly extProxy: TerminalServiceExt; - private terminalNumber = 0; - private readonly TERM_ID_PREFIX = 'plugin-terminal-'; constructor(rpc: RPCProtocol, container: interfaces.Container) { - this.terminalService = container.get(TerminalService); + this.terminals = container.get(TerminalService); this.shell = container.get(ApplicationShell); this.extProxy = rpc.getProxy(MAIN_RPC_CONTEXT.TERMINAL_EXT); + this.terminals.onDidCreateTerminal(terminal => this.trackTerminal(terminal)); + for (const terminal of this.terminals.all) { + this.trackTerminal(terminal); + } + this.terminals.onDidChangeCurrentTerminal(() => this.updateCurrentTerminal()); + this.updateCurrentTerminal(); } - async $createTerminal(options: TerminalOptions): Promise { - const counter = this.terminalNumber++; - const termWidgetOptions: TerminalWidgetOptions = { - title: options.name, - shellPath: options.shellPath, - shellArgs: options.shellArgs, - cwd: options.cwd, - env: options.env, - destroyTermOnClose: true, - useServerTitle: false, - id: this.TERM_ID_PREFIX + counter, - attributes: options.attributes - }; - let id: number; + protected updateCurrentTerminal(): void { + const { currentTerminal } = this.terminals; + this.extProxy.$currentTerminalChanged(currentTerminal && currentTerminal.id); + } + + protected async trackTerminal(terminal: TerminalWidget): Promise { + let name = terminal.title.label; + this.extProxy.$terminalCreated(terminal.id, name); + terminal.title.changed.connect(() => { + if (name !== terminal.title.label) { + name = terminal.title.label; + this.extProxy.$terminalNameChanged(terminal.id, name); + } + }); + const updateProcessId = () => terminal.processId.then( + processId => this.extProxy.$terminalOpened(terminal.id, processId), + () => {/*no-op*/ } + ); + updateProcessId(); + terminal.onDidOpen(() => updateProcessId()); + terminal.onTerminalDidClose(() => this.extProxy.$terminalClosed(terminal.id)); + } + + async $createTerminal(id: string, options: TerminalOptions): Promise { try { - const termWidget = await this.terminalService.newTerminal(termWidgetOptions); - id = await termWidget.start(); - this.terminals.set(id, termWidget); - termWidget.onTerminalDidClose(() => { - this.extProxy.$terminalClosed(id); + const terminal = await this.terminals.newTerminal({ + id, + title: options.name, + shellPath: options.shellPath, + shellArgs: options.shellArgs, + cwd: options.cwd, + env: options.env, + destroyTermOnClose: true, + useServerTitle: false, + attributes: options.attributes }); + terminal.start(); + return terminal.id; } catch (error) { throw new Error('Failed to create terminal. Cause: ' + error); } - return id; } - $sendText(id: number, text: string, addNewLine?: boolean): void { - const termWidget = this.terminals.get(id); - if (termWidget) { + $sendText(id: string, text: string, addNewLine?: boolean): void { + const terminal = this.terminals.getById(id); + if (terminal) { text = text.replace(/\r?\n/g, '\r'); if (addNewLine && text.charAt(text.length - 1) !== '\r') { text += '\r'; } - termWidget.sendText(text); + terminal.sendText(text); } } - $show(id: number, preserveFocus?: boolean): void { - const termWidget = this.terminals.get(id); - if (termWidget) { - this.terminalService.activateTerminal(termWidget); + $show(id: string, preserveFocus?: boolean): void { + const terminal = this.terminals.getById(id); + if (terminal) { + const options: WidgetOpenerOptions = {}; + if (preserveFocus) { + options.mode = 'reveal'; + } + this.terminals.open(terminal, options); } } - $hide(id: number): void { - const termWidget = this.terminals.get(id); - if (termWidget) { - if (termWidget.isVisible) { - const area = this.shell.getAreaFor(termWidget); - if (area) { - this.shell.collapsePanel(area); - } + $hide(id: string): void { + const terminal = this.terminals.getById(id); + if (terminal && terminal.isVisible) { + const area = this.shell.getAreaFor(terminal); + if (area) { + this.shell.collapsePanel(area); } } } - $dispose(id: number): void { - const termWidget = this.terminals.get(id); - if (termWidget) { - termWidget.dispose(); + $dispose(id: string): void { + const terminal = this.terminals.getById(id); + if (terminal) { + terminal.dispose(); } } } diff --git a/packages/plugin-ext/src/plugin/plugin-context.ts b/packages/plugin-ext/src/plugin/plugin-context.ts index 56ececdfdfa63..b3d64da835ab3 100644 --- a/packages/plugin-ext/src/plugin/plugin-context.ts +++ b/packages/plugin-ext/src/plugin/plugin-context.ts @@ -166,13 +166,21 @@ export function createAPIFactory( } }; + const { onDidChangeActiveTerminal, onDidCloseTerminal, onDidOpenTerminal } = terminalExt; const window: typeof theia.window = { + get activeTerminal() { + return terminalExt.activeTerminal; + }, get activeTextEditor() { return editors.getActiveEditor(); }, get visibleTextEditors() { return editors.getVisibleTextEditors(); }, + get terminals() { + return terminalExt.terminals; + }, + onDidChangeActiveTerminal, onDidChangeActiveTextEditor(listener, thisArg?, disposables?) { return editors.onDidChangeActiveTextEditor(listener, thisArg, disposables); }, @@ -293,12 +301,8 @@ export function createAPIFactory( createTerminal(nameOrOptions: theia.TerminalOptions | (string | undefined), shellPath?: string, shellArgs?: string[]): theia.Terminal { return terminalExt.createTerminal(nameOrOptions, shellPath, shellArgs); }, - get onDidCloseTerminal(): theia.Event { - return terminalExt.onDidCloseTerminal; - }, - set onDidCloseTerminal(event: theia.Event) { - terminalExt.onDidCloseTerminal = event; - }, + onDidCloseTerminal, + onDidOpenTerminal, createTextEditorDecorationType(options: theia.DecorationRenderOptions): theia.TextEditorDecorationType { return editors.createTextEditorDecorationType(options); }, diff --git a/packages/plugin-ext/src/plugin/terminal-ext.ts b/packages/plugin-ext/src/plugin/terminal-ext.ts index f47adb1cb373e..a73e2711f9a95 100644 --- a/packages/plugin-ext/src/plugin/terminal-ext.ts +++ b/packages/plugin-ext/src/plugin/terminal-ext.ts @@ -13,10 +13,12 @@ * * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ +import { UUID } from '@phosphor/coreutils/lib/uuid'; import { Terminal, TerminalOptions } from '@theia/plugin'; import { TerminalServiceExt, TerminalServiceMain, PLUGIN_RPC_CONTEXT } from '../api/plugin-api'; import { RPCProtocol } from '../api/rpc-protocol'; import { Emitter } from '@theia/core/lib/common/event'; +import { Deferred } from '@theia/core/lib/common/promise-util'; import * as theia from '@theia/plugin'; /** @@ -26,13 +28,26 @@ import * as theia from '@theia/plugin'; export class TerminalServiceExtImpl implements TerminalServiceExt { private readonly proxy: TerminalServiceMain; - private readonly terminals: Map = new Map(); + + private readonly _terminals = new Map(); + private readonly onDidCloseTerminalEmitter = new Emitter(); + readonly onDidCloseTerminal: theia.Event = this.onDidCloseTerminalEmitter.event; + + private readonly onDidOpenTerminalEmitter = new Emitter(); + readonly onDidOpenTerminal: theia.Event = this.onDidOpenTerminalEmitter.event; + + private readonly onDidChangeActiveTerminalEmitter = new Emitter(); + readonly onDidChangeActiveTerminal: theia.Event = this.onDidChangeActiveTerminalEmitter.event; constructor(rpc: RPCProtocol) { this.proxy = rpc.getProxy(PLUGIN_RPC_CONTEXT.TERMINAL_MAIN); } + get terminals(): TerminalExtImpl[] { + return [...this._terminals.values()]; + } + createTerminal(nameOrOptions: TerminalOptions | (string | undefined), shellPath?: string, shellArgs?: string[]): Terminal { let options: TerminalOptions; if (typeof nameOrOptions === 'object') { @@ -44,58 +59,90 @@ export class TerminalServiceExtImpl implements TerminalServiceExt { shellArgs: shellArgs }; } + const id = `plugin-terminal-${UUID.uuid4()}`; + this.proxy.$createTerminal(id, options); + return this.obtainTerminal(id, options.name || 'Terminal'); + } - const terminal = new TerminalExtImpl(this.proxy, options.name || 'Terminal'); - terminal.create(options, shellPath, shellArgs); - terminal.processId.then(id => { - this.terminals.set(id, terminal); - }); + protected obtainTerminal(id: string, name: string): TerminalExtImpl { + let terminal = this._terminals.get(id); + if (!terminal) { + terminal = new TerminalExtImpl(this.proxy); + this._terminals.set(id, terminal); + } + terminal.name = name; return terminal; } - $terminalClosed(id: number): void { - const terminal = this.terminals.get(id); + $terminalCreated(id: string, name: string): void { + const terminal = this.obtainTerminal(id, name); + this.onDidOpenTerminalEmitter.fire(terminal); + } + + $terminalNameChanged(id: string, name: string): void { + const terminal = this._terminals.get(id); if (terminal) { - this.onDidCloseTerminalEmitter.fire(terminal); + terminal.name = name; } } - public set onDidCloseTerminal(event: theia.Event) { - this.onDidCloseTerminalEmitter.event.apply(event); + $terminalOpened(id: string, processId: number): void { + const terminal = this._terminals.get(id); + if (terminal) { + // resolve for existing clients + terminal.deferredProcessId.resolve(processId); + // install new if terminal is reconnected + terminal.deferredProcessId = new Deferred(); + terminal.deferredProcessId.resolve(processId); + } } - public get onDidCloseTerminal(): theia.Event { - return this.onDidCloseTerminalEmitter.event; + $terminalClosed(id: string): void { + const terminal = this._terminals.get(id); + if (terminal) { + this.onDidCloseTerminalEmitter.fire(terminal); + this._terminals.delete(id); + } } + + private activeTerminalId: string | undefined; + get activeTerminal(): TerminalExtImpl | undefined { + return this.activeTerminalId && this._terminals.get(this.activeTerminalId) || undefined; + } + $currentTerminalChanged(id: string | undefined): void { + this.activeTerminalId = id; + this.onDidChangeActiveTerminalEmitter.fire(this.activeTerminal); + } + } export class TerminalExtImpl implements Terminal { - termProcessId: PromiseLike; + name: string; - constructor(private readonly proxy: TerminalServiceMain, readonly name: string) { } + readonly id = new Deferred(); - create(nameOrOptions: TerminalOptions, shellPath?: string, shellArgs?: string[]): void { - this.termProcessId = this.proxy.$createTerminal(nameOrOptions); + deferredProcessId = new Deferred(); + get processId(): Thenable { + return this.deferredProcessId.promise; } + constructor(private readonly proxy: TerminalServiceMain) { } + sendText(text: string, addNewLine: boolean = true): void { - this.termProcessId.then(id => this.proxy.$sendText(id, text, addNewLine)); + this.id.promise.then(id => this.proxy.$sendText(id, text, addNewLine)); } show(preserveFocus?: boolean): void { - this.termProcessId.then(id => this.proxy.$show(id)); + this.id.promise.then(id => this.proxy.$show(id, preserveFocus)); } hide(): void { - this.termProcessId.then(id => this.proxy.$hide(id)); + this.id.promise.then(id => this.proxy.$hide(id)); } dispose(): void { - this.termProcessId.then(id => this.proxy.$dispose(id)); + this.id.promise.then(id => this.proxy.$dispose(id)); } - public get processId(): Thenable { - return this.termProcessId; - } } diff --git a/packages/plugin/src/theia.d.ts b/packages/plugin/src/theia.d.ts index 170870ac7e1a0..1463308aab275 100644 --- a/packages/plugin/src/theia.d.ts +++ b/packages/plugin/src/theia.d.ts @@ -2657,6 +2657,12 @@ declare module '@theia/plugin' { */ export namespace window { + /** + * The currently active terminal or undefined. The active terminal is the one + * that currently has focus or most recently had focus. + */ + export let activeTerminal: Terminal | undefined; + /** * The currently active editor or `undefined`. The active editor is the one * that currently has focus or, when none has focus, the one that has changed @@ -2664,11 +2670,22 @@ declare module '@theia/plugin' { */ export let activeTextEditor: TextEditor | undefined; + /** + * The currently opened terminals or an empty array. + */ + export let terminals: ReadonlyArray; + /** * The currently visible editors or an empty array. */ export let visibleTextEditors: TextEditor[]; + /** + * An [event](#Event) which fires when the [active terminal](#window.activeTerminal) has changed. + * *Note* that the event also fires when the active terminal changes to `undefined`. + */ + export const onDidChangeActiveTerminal: Event; + /** * An [event](#Event) which fires when the [active editor](#window.activeTextEditor) * has changed. *Note* that the event also fires when the active editor changes @@ -3024,6 +3041,12 @@ declare module '@theia/plugin' { */ export const onDidCloseTerminal: Event; + /** + * An [event](#Event) which fires when a terminal has been created, + * either through the createTerminal API or commands. + */ + export const onDidOpenTerminal: Event; + /** * Create new terminal with predefined options. * @param - terminal options. @@ -3904,8 +3927,8 @@ declare module '@theia/plugin' { * @return A thenable that resolves when the edit could be applied. */ export function applyEdit(edit: WorkspaceEdit): PromiseLike; - - + + /** * Register a filesystem provider for a given scheme, e.g. `ftp`. * diff --git a/packages/terminal/src/browser/base/terminal-service.ts b/packages/terminal/src/browser/base/terminal-service.ts index c57ba7a4e2e45..ad843e33aaece 100644 --- a/packages/terminal/src/browser/base/terminal-service.ts +++ b/packages/terminal/src/browser/base/terminal-service.ts @@ -13,6 +13,8 @@ * * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ +import { Event } from '@theia/core/lib/common/event'; +import { WidgetOpenerOptions } from '@theia/core/lib/browser'; import { TerminalWidgetOptions, TerminalWidget } from './terminal-widget'; /** @@ -20,6 +22,7 @@ import { TerminalWidgetOptions, TerminalWidget } from './terminal-widget'; */ export const TerminalService = Symbol('TerminalService'); export interface TerminalService { + /** * Create new terminal with predefined options. * @param options - terminal options. @@ -28,7 +31,20 @@ export interface TerminalService { /** * Display new terminal widget. - * @param termWidget - widget to attach. + * @param terminal - widget to attach. + * @deprecated use #open */ - activateTerminal(termWidget: TerminalWidget): void; + activateTerminal(terminal: TerminalWidget): void; + + open(terminal: TerminalWidget, options?: WidgetOpenerOptions): void; + + readonly all: TerminalWidget[]; + + getById(id: string): TerminalWidget | undefined; + + readonly onDidCreateTerminal: Event; + + readonly currentTerminal: TerminalWidget | undefined; + + readonly onDidChangeCurrentTerminal: Event; } diff --git a/packages/terminal/src/browser/base/terminal-widget.ts b/packages/terminal/src/browser/base/terminal-widget.ts index fbaf15eea5d1e..e63c91b851f02 100644 --- a/packages/terminal/src/browser/base/terminal-widget.ts +++ b/packages/terminal/src/browser/base/terminal-widget.ts @@ -22,27 +22,31 @@ import { BaseWidget } from '@theia/core/lib/browser'; */ export abstract class TerminalWidget extends BaseWidget { + abstract processId: Promise; + /** * Start terminal and return terminal id. * @param id - terminal id. */ - abstract start(id?: number): Promise; - - /** - * Send text to the terminal server. - * @param text - text content. - */ - abstract sendText(text: string): void; - - /** - * Event which fires when terminal did closed. Event value contains closed terminal widget definition. - */ - abstract onTerminalDidClose: Event; - - /** - * Cleat terminal output. - */ - abstract clearOutput(): void; + abstract start(id?: number): Promise; + + /** + * Send text to the terminal server. + * @param text - text content. + */ + abstract sendText(text: string): void; + + abstract onDidOpen: Event; + + /** + * Event which fires when terminal did closed. Event value contains closed terminal widget definition. + */ + abstract onTerminalDidClose: Event; + + /** + * Cleat terminal output. + */ + abstract clearOutput(): void; } /** diff --git a/packages/terminal/src/browser/terminal-frontend-contribution.ts b/packages/terminal/src/browser/terminal-frontend-contribution.ts index 69a8d9ad0ee32..944605d7641ab 100644 --- a/packages/terminal/src/browser/terminal-frontend-contribution.ts +++ b/packages/terminal/src/browser/terminal-frontend-contribution.ts @@ -14,7 +14,7 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { inject, injectable } from 'inversify'; +import { inject, injectable, postConstruct } from 'inversify'; import { CommandContribution, Command, @@ -22,11 +22,13 @@ import { MenuContribution, MenuModelRegistry, isOSX, - SelectionService + SelectionService, + Emitter, Event } from '@theia/core/lib/common'; +import { QuickPickService } from '@theia/core/lib/common/quick-pick-service'; import { ApplicationShell, KeybindingContribution, KeyCode, Key, - KeyModifier, KeybindingRegistry, Widget, QuickPickService, LabelProvider + KeyModifier, KeybindingRegistry, Widget, LabelProvider, WidgetOpenerOptions } from '@theia/core/lib/browser'; import { TabBarToolbarContribution, TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar'; import { WidgetManager } from '@theia/core/lib/browser'; @@ -95,6 +97,50 @@ export class TerminalFrontendContribution implements TerminalService, CommandCon @inject(WorkspaceService) protected readonly workspaceService: WorkspaceService; + protected readonly onDidCreateTerminalEmitter = new Emitter(); + readonly onDidCreateTerminal: Event = this.onDidCreateTerminalEmitter.event; + + protected readonly onDidChangeCurrentTerminalEmitter = new Emitter(); + readonly onDidChangeCurrentTerminal: Event = this.onDidCreateTerminalEmitter.event; + + @postConstruct() + protected init(): void { + this.shell.currentChanged.connect(() => this.updateCurrentTerminal()); + this.widgetManager.onDidCreateWidget(({ widget }) => { + if (widget instanceof TerminalWidget) { + this.updateCurrentTerminal(); + this.onDidCreateTerminalEmitter.fire(widget); + } + }); + } + + protected _currentTerminal: TerminalWidget | undefined; + get currentTerminal(): TerminalWidget | undefined { + return this._currentTerminal; + } + protected setCurrentTerminal(current: TerminalWidget | undefined): void { + if (this._currentTerminal !== current) { + this._currentTerminal = current; + this.onDidChangeCurrentTerminalEmitter.fire(this._currentTerminal); + } + } + protected updateCurrentTerminal(): void { + const widget = this.shell.currentWidget; + if (widget instanceof TerminalWidget) { + this.setCurrentTerminal(widget); + } else if (!this._currentTerminal || !this._currentTerminal.isVisible) { + this.setCurrentTerminal(undefined); + } + } + + get all(): TerminalWidget[] { + return this.widgetManager.getWidgets(TERMINAL_WIDGET_FACTORY_ID) as TerminalWidget[]; + } + + getById(id: string): TerminalWidget | undefined { + return this.all.find(terminal => terminal.id === id); + } + registerCommands(commands: CommandRegistry): void { commands.registerCommand(TerminalCommands.NEW, { execute: () => this.openTerminal() @@ -265,12 +311,28 @@ export class TerminalFrontendContribution implements TerminalService, CommandCon return widget; } - activateTerminal(widget: TerminalWidget, options: ApplicationShell.WidgetOptions = { area: 'bottom' }): void { - const tabBar = this.shell.getTabBarFor(widget); - if (!tabBar) { - this.shell.addWidget(widget, options); + activateTerminal(widget: TerminalWidget, widgetOptions?: ApplicationShell.WidgetOptions): void { + this.open(widget, { widgetOptions }); + } + + // TODO: reuse WidgetOpenHandler.open + open(widget: TerminalWidget, options?: WidgetOpenerOptions): void { + const op: WidgetOpenerOptions = { + mode: 'activate', + ...options, + widgetOptions: { + area: 'bottom', + ...(options && options.widgetOptions) + } + }; + if (!widget.isAttached) { + this.shell.addWidget(widget, op.widgetOptions); + } + if (op.mode === 'activate') { + this.shell.activateWidget(widget.id); + } else if (op.mode === 'reveal') { + this.shell.revealWidget(widget.id); } - this.shell.activateWidget(widget.id); } protected async selectTerminalCwd(): Promise { @@ -296,12 +358,12 @@ export class TerminalFrontendContribution implements TerminalService, CommandCon const cwd = await this.selectTerminalCwd(); const termWidget = await this.newTerminal({ cwd }); termWidget.start(); - this.activateTerminal(termWidget, options); + this.open(termWidget, { widgetOptions: options }); } protected async openActiveWorkspaceTerminal(options?: ApplicationShell.WidgetOptions): Promise { const termWidget = await this.newTerminal({}); termWidget.start(); - this.activateTerminal(termWidget, options); + this.open(termWidget, { widgetOptions: options }); } } diff --git a/packages/terminal/src/browser/terminal-widget-impl.ts b/packages/terminal/src/browser/terminal-widget-impl.ts index fa87c21a56e2f..cf7800922d1cc 100644 --- a/packages/terminal/src/browser/terminal-widget-impl.ts +++ b/packages/terminal/src/browser/terminal-widget-impl.ts @@ -60,7 +60,7 @@ export class TerminalWidgetImpl extends TerminalWidget implements StatefulWidget private readonly TERMINAL = 'Terminal'; protected readonly onTermDidClose = new Emitter(); - protected terminalId: number; + protected terminalId = -1; protected term: Xterm.Terminal; protected restored = false; protected closeOnDispose = true; @@ -76,6 +76,9 @@ export class TerminalWidgetImpl extends TerminalWidget implements StatefulWidget @inject('terminal-dom-id') public readonly id: string; @inject(TerminalPreferences) protected readonly preferences: TerminalPreferences; + protected readonly onDidOpenEmitter = new Emitter(); + readonly onDidOpen: Event = this.onDidOpenEmitter.event; + protected readonly toDisposeOnConnect = new DisposableCollection(); @postConstruct() @@ -127,11 +130,11 @@ export class TerminalWidgetImpl extends TerminalWidget implements StatefulWidget this.toDispose.push(this.terminalWatcher.onTerminalError(({ terminalId, error }) => { if (terminalId === this.terminalId) { - this.dispose(); - this.onTermDidClose.fire(this); - this.onTermDidClose.dispose(); - this.logger.error(`The terminal process terminated. Cause: ${error}`); - } + this.dispose(); + this.onTermDidClose.fire(this); + this.onTermDidClose.dispose(); + this.logger.error(`The terminal process terminated. Cause: ${error}`); + } })); this.toDispose.push(this.terminalWatcher.onTerminalExit(({ terminalId }) => { if (terminalId === this.terminalId) { @@ -149,6 +152,16 @@ export class TerminalWidgetImpl extends TerminalWidget implements StatefulWidget this.toDispose.push(disposable); })); this.toDispose.push(this.onTermDidClose); + this.toDispose.push(this.onDidOpenEmitter); + } + + get processId(): Promise { + return (async () => { + if (!IBaseTerminalServer.validateId(this.terminalId)) { + throw new Error('terminal is not started'); + } + return this.shellTerminalServer.getProcessId(this.terminalId); + })(); } clearOutput(): void { @@ -233,6 +246,7 @@ export class TerminalWidgetImpl extends TerminalWidget implements StatefulWidget this.resizeTerminalProcess(); this.connectTerminalProcess(); if (IBaseTerminalServer.validateId(this.terminalId)) { + this.onDidOpenEmitter.fire(undefined); return this.terminalId; } throw new Error('Failed to start terminal' + (id ? ` for id: ${id}.` : '.')); diff --git a/packages/terminal/src/common/base-terminal-protocol.ts b/packages/terminal/src/common/base-terminal-protocol.ts index bdc09a12e1d59..3f3a442645467 100644 --- a/packages/terminal/src/common/base-terminal-protocol.ts +++ b/packages/terminal/src/common/base-terminal-protocol.ts @@ -21,6 +21,7 @@ export interface IBaseTerminalServerOptions { } export interface IBaseTerminalServer extends JsonRpcServer { create(IBaseTerminalServerOptions: object): Promise; + getProcessId(id: number): Promise; resize(id: number, cols: number, rows: number): Promise; attach(id: number): Promise; close(id: number): Promise; diff --git a/packages/terminal/src/node/base-terminal-server.ts b/packages/terminal/src/node/base-terminal-server.ts index aac006eccca0d..0237561d1857c 100644 --- a/packages/terminal/src/node/base-terminal-server.ts +++ b/packages/terminal/src/node/base-terminal-server.ts @@ -50,6 +50,14 @@ export abstract class BaseTerminalServer implements IBaseTerminalServer { } } + async getProcessId(id: number): Promise { + const terminal = this.processManager.get(id); + if (!(terminal instanceof TerminalProcess)) { + throw new Error(`terminal "${id}" does not exist`); + } + return terminal.pid; + } + async close(id: number): Promise { const term = this.processManager.get(id);