diff --git a/packages/plugin-ext/src/common/plugin-api-rpc.ts b/packages/plugin-ext/src/common/plugin-api-rpc.ts index 6f8ec9fd68506..8a4f588ef69bd 100644 --- a/packages/plugin-ext/src/common/plugin-api-rpc.ts +++ b/packages/plugin-ext/src/common/plugin-api-rpc.ts @@ -303,7 +303,7 @@ export interface TerminalServiceMain { * Create new Terminal with Terminal options. * @param options - object with parameters to create new terminal. */ - $createTerminal(id: string, options: theia.TerminalOptions, isPseudoTerminal?: boolean): Promise; + $createTerminal(id: string, options: theia.TerminalOptions, parentId?: string, isPseudoTerminal?: boolean): Promise; /** * Send text to the terminal by id. diff --git a/packages/plugin-ext/src/main/browser/terminal-main.ts b/packages/plugin-ext/src/main/browser/terminal-main.ts index 65a7534d02ca2..b99415ff4a936 100644 --- a/packages/plugin-ext/src/main/browser/terminal-main.ts +++ b/packages/plugin-ext/src/main/browser/terminal-main.ts @@ -16,9 +16,9 @@ import { interfaces } from '@theia/core/shared/inversify'; import { ApplicationShell, WidgetOpenerOptions } from '@theia/core/lib/browser'; -import { TerminalOptions } from '@theia/plugin'; import { CancellationToken } from '@theia/core/shared/vscode-languageserver-protocol'; -import { TerminalWidget } from '@theia/terminal/lib/browser/base/terminal-widget'; +import { TerminalEditorLocationOptions, TerminalOptions } from '@theia/plugin'; +import { TerminalLocation, TerminalWidget } from '@theia/terminal/lib/browser/base/terminal-widget'; import { TerminalService } from '@theia/terminal/lib/browser/base/terminal-service'; import { TerminalServiceMain, TerminalServiceExt, MAIN_RPC_CONTEXT } from '../../common/plugin-api-rpc'; import { RPCProtocol } from '../../common/rpc-protocol'; @@ -122,7 +122,7 @@ export class TerminalServiceMainImpl implements TerminalServiceMain, TerminalLin terminal.resize(cols, rows); } - async $createTerminal(id: string, options: TerminalOptions, isPseudoTerminal?: boolean): Promise { + async $createTerminal(id: string, options: TerminalOptions, parentId?: string, isPseudoTerminal?: boolean): Promise { try { const terminal = await this.terminals.newTerminal({ id, @@ -136,6 +136,7 @@ export class TerminalServiceMainImpl implements TerminalServiceMain, TerminalLin useServerTitle: false, attributes: options.attributes, hideFromUser: options.hideFromUser, + location: this.getTerminalLocation(options, parentId), isPseudoTerminal }); if (options.message) { @@ -148,6 +149,24 @@ export class TerminalServiceMainImpl implements TerminalServiceMain, TerminalLin } } + protected getTerminalLocation(options: TerminalOptions, parentId?: string): TerminalLocation | TerminalEditorLocationOptions | { parentTerminal: string; } | undefined { + if (options.location && typeof options.location === 'number') { + return options.location; + } else if (options.location && typeof options.location === 'object') { + if ('parentTerminal' in options.location) { + if (!parentId) { + throw new Error('parentTerminal is set but no parentId is provided'); + } + console.log('parentId ' + parentId); + return { 'parentTerminal': parentId }; + } else { + return options.location; + } + } + + return undefined; + } + $sendText(id: string, text: string, addNewLine?: boolean): void { const terminal = this.terminals.getById(id); if (terminal) { diff --git a/packages/plugin-ext/src/plugin/plugin-context.ts b/packages/plugin-ext/src/plugin/plugin-context.ts index 69432c6f3f47d..9c777b3efe9b3 100644 --- a/packages/plugin-ext/src/plugin/plugin-context.ts +++ b/packages/plugin-ext/src/plugin/plugin-context.ts @@ -152,6 +152,7 @@ import { TextDocumentChangeReason, InputBoxValidationSeverity, TerminalLink, + TerminalLocation, InlayHint, InlayHintKind, InlayHintLabelPart, @@ -1135,7 +1136,8 @@ export function createAPIFactory( ExtensionKind, InlineCompletionItem, InlineCompletionList, - InlineCompletionTriggerKind + InlineCompletionTriggerKind, + TerminalLocation }; }; } diff --git a/packages/plugin-ext/src/plugin/terminal-ext.ts b/packages/plugin-ext/src/plugin/terminal-ext.ts index 70e75922faa14..b59aaa04b792c 100644 --- a/packages/plugin-ext/src/plugin/terminal-ext.ts +++ b/packages/plugin-ext/src/plugin/terminal-ext.ts @@ -85,7 +85,22 @@ export class TerminalServiceExtImpl implements TerminalServiceExt { shellArgs: shellArgs }; } - this.proxy.$createTerminal(id, options, !!pseudoTerminal); + + let parentId; + + if (options.location && typeof options.location === 'object' && 'parentTerminal' in options.location) { + const parentTerminal = options.location.parentTerminal; + if (parentTerminal instanceof TerminalExtImpl) { + for (const [k, v] of this._terminals) { + if (v === parentTerminal) { + parentId = k; + break; + } + } + } + } + + this.proxy.$createTerminal(id, options, parentId, !!pseudoTerminal); let creationOptions: theia.TerminalOptions | theia.ExtensionTerminalOptions = options; // make sure to pass ExtensionTerminalOptions as creation options diff --git a/packages/plugin-ext/src/plugin/types-impl.ts b/packages/plugin-ext/src/plugin/types-impl.ts index 78cbc2204fe20..75b2313929128 100644 --- a/packages/plugin-ext/src/plugin/types-impl.ts +++ b/packages/plugin-ext/src/plugin/types-impl.ts @@ -1695,6 +1695,11 @@ export class TerminalLink { } } +export enum TerminalLocation { + Panel = 1, + Editor = 2 +} + @es5ClassCompat export class FileDecoration { diff --git a/packages/plugin/src/theia.d.ts b/packages/plugin/src/theia.d.ts index 0422b0d005d7f..0df51c0c2b5c0 100644 --- a/packages/plugin/src/theia.d.ts +++ b/packages/plugin/src/theia.d.ts @@ -3001,6 +3001,11 @@ export module '@theia/plugin' { */ message?: string; + /** + * The {@link TerminalLocation} or {@link TerminalEditorLocationOptions} or {@link TerminalSplitLocationOptions} for the terminal. + */ + location?: TerminalLocation | TerminalEditorLocationOptions | TerminalSplitLocationOptions; + /** * Terminal attributes. Can be useful to apply some implementation specific information. */ @@ -3067,6 +3072,11 @@ export module '@theia/plugin' { * control it. */ pty: Pseudoterminal; + + /** + * The {@link TerminalLocation} or {@link TerminalEditorLocationOptions} or {@link TerminalSplitLocationOptions} for the terminal. + */ + location?: TerminalLocation | TerminalEditorLocationOptions | TerminalSplitLocationOptions; } /** @@ -3207,6 +3217,50 @@ export module '@theia/plugin' { constructor(startIndex: number, length: number, tooltip?: string); } + /** + * The location of the {@link Terminal}. + */ + export enum TerminalLocation { + /** + * In the terminal view + */ + Panel = 1, + /** + * In the editor area + */ + Editor = 2, + } + + /** + * Assumes a {@link TerminalLocation} of editor and allows specifying a {@link ViewColumn} and + * {@link TerminalEditorLocationOptions.preserveFocus preserveFocus } property + */ + export interface TerminalEditorLocationOptions { + /** + * A view column in which the {@link Terminal terminal} should be shown in the editor area. + * Use {@link ViewColumn.Active active} to open in the active editor group, other values are + * adjusted to be `Min(column, columnCount + 1)`, the + * {@link ViewColumn.Active active}-column is not adjusted. Use + * {@linkcode ViewColumn.Beside} to open the editor to the side of the currently active one. + */ + viewColumn: ViewColumn; + /** + * An optional flag that when `true` will stop the {@link Terminal} from taking focus. + */ + preserveFocus?: boolean; + } + + /** + * Uses the parent {@link Terminal}'s location for the terminal + */ + export interface TerminalSplitLocationOptions { + /** + * The parent terminal to split this terminal beside. This works whether the parent terminal + * is in the panel or the editor area. + */ + parentTerminal: Terminal; + } + /** * A file decoration represents metadata that can be rendered with a file. */ diff --git a/packages/terminal/src/browser/base/terminal-widget.ts b/packages/terminal/src/browser/base/terminal-widget.ts index f1bf943479874..de6a02d1d9143 100644 --- a/packages/terminal/src/browser/base/terminal-widget.ts +++ b/packages/terminal/src/browser/base/terminal-widget.ts @@ -30,13 +30,79 @@ export interface TerminalExitStatus { readonly code: number | undefined; } +export type TerminalLocationOptions = TerminalLocation | TerminalEditorLocation | TerminalSplitLocation; + +export enum TerminalLocation { + Panel = 1, + Editor = 2 +} + +export interface TerminalEditorLocation { + readonly viewColumn: number; + readonly preserveFocus?: boolean; +} + +export interface TerminalSplitLocation { + readonly parentTerminal: string; +} + +export enum ViewColumn { + /** + * A *symbolic* editor column representing the currently active column. This value + * can be used when opening editors, but the *resolved* {@link TextEditor.viewColumn viewColumn}-value + * of editors will always be `One`, `Two`, `Three`,... or `undefined` but never `Active`. + */ + Active = -1, + /** + * A *symbolic* editor column representing the column to the side of the active one. This value + * can be used when opening editors, but the *resolved* {@link TextEditor.viewColumn viewColumn}-value + * of editors will always be `One`, `Two`, `Three`,... or `undefined` but never `Beside`. + */ + Beside = -2, + /** + * The first editor column. + */ + One = 1, + /** + * The second editor column. + */ + Two = 2, + /** + * The third editor column. + */ + Three = 3, + /** + * The fourth editor column. + */ + Four = 4, + /** + * The fifth editor column. + */ + Five = 5, + /** + * The sixth editor column. + */ + Six = 6, + /** + * The seventh editor column. + */ + Seven = 7, + /** + * The eighth editor column. + */ + Eight = 8, + /** + * The ninth editor column. + */ + Nine = 9 +} + /** * Terminal UI widget. */ export abstract class TerminalWidget extends BaseWidget { abstract processId: Promise; - /** * Get the current executable and arguments. */ @@ -54,6 +120,9 @@ export abstract class TerminalWidget extends BaseWidget { /** Terminal widget can be hidden from users until explicitly shown once. */ abstract readonly hiddenFromUser: boolean; + /** The position of the terminal widget. */ + abstract readonly location: TerminalLocationOptions; + /** The last CWD assigned to the terminal, useful when attempting getCwdURI on a task terminal fails */ lastCwd: URI; @@ -211,4 +280,6 @@ export interface TerminalWidgetOptions { * When enabled the terminal will run the process as normal but not be surfaced to the user until `Terminal.show` is called. */ readonly hideFromUser?: boolean; + + readonly location?: TerminalLocationOptions; } diff --git a/packages/terminal/src/browser/terminal-frontend-contribution.ts b/packages/terminal/src/browser/terminal-frontend-contribution.ts index f17e86fa1b177..315e79b4552d4 100644 --- a/packages/terminal/src/browser/terminal-frontend-contribution.ts +++ b/packages/terminal/src/browser/terminal-frontend-contribution.ts @@ -36,7 +36,7 @@ import { TabBarToolbarContribution, TabBarToolbarRegistry } from '@theia/core/li import { TERMINAL_WIDGET_FACTORY_ID, TerminalWidgetFactoryOptions, TerminalWidgetImpl } from './terminal-widget-impl'; import { TerminalKeybindingContexts } from './terminal-keybinding-contexts'; import { TerminalService } from './base/terminal-service'; -import { TerminalWidgetOptions, TerminalWidget } from './base/terminal-widget'; +import { TerminalWidgetOptions, TerminalWidget, TerminalLocation, ViewColumn } from './base/terminal-widget'; import { UriAwareCommandHandler } from '@theia/core/lib/common/uri-command-handler'; import { ShellTerminalServerProxy } from '../common/shell-terminal-protocol'; import URI from '@theia/core/lib/common/uri'; @@ -644,20 +644,50 @@ export class TerminalFrontendContribution implements FrontendApplicationContribu // TODO: reuse WidgetOpenHandler.open open(widget: TerminalWidget, options?: WidgetOpenerOptions): void { + const area = widget.location === TerminalLocation.Editor ? 'main' : 'bottom'; + const widgetOptions: ApplicationShell.WidgetOptions = { area: area, ...(options && options.widgetOptions) }; + let preserveFocus = false; + + if (typeof widget.location === 'object') { + if ('parentTerminal' in widget.location) { + widgetOptions.ref = this.getById(widget.location.parentTerminal); + widgetOptions.mode = 'split-right'; + } else if ('viewColumn' in widget.location) { + preserveFocus = widget.location.preserveFocus ?? false; + switch (widget.location.viewColumn) { + case ViewColumn.Active: + widgetOptions.ref = this.shell.currentWidget; + widgetOptions.mode = 'tab-before'; + break; + case ViewColumn.Beside: + widgetOptions.ref = this.shell.currentWidget; + widgetOptions.mode = 'tab-after'; + break; + default: + const widgets = this.all.filter(t => t.isVisible); + const index = widget.location.viewColumn - 1; + if (index < widgets.length) { + widgetOptions.ref = widgets[index]; + widgetOptions.mode = 'open-to-left'; + } else { + widgetOptions.ref = widgets[widgets.length - 1]; + widgetOptions.mode = 'open-to-right'; + } + } + } + } + const op: WidgetOpenerOptions = { mode: 'activate', ...options, - widgetOptions: { - area: 'bottom', - ...(options && options.widgetOptions) - } + widgetOptions: widgetOptions }; if (!widget.isAttached) { this.shell.addWidget(widget, op.widgetOptions); } - if (op.mode === 'activate') { + if (op.mode === 'activate' && !preserveFocus) { this.shell.activateWidget(widget.id); - } else if (op.mode === 'reveal') { + } else if (op.mode === 'reveal' || preserveFocus) { this.shell.revealWidget(widget.id); } } diff --git a/packages/terminal/src/browser/terminal-widget-impl.ts b/packages/terminal/src/browser/terminal-widget-impl.ts index 7b0448aab6266..490bd1aaf352f 100644 --- a/packages/terminal/src/browser/terminal-widget-impl.ts +++ b/packages/terminal/src/browser/terminal-widget-impl.ts @@ -25,7 +25,7 @@ import { ShellTerminalServerProxy, IShellTerminalPreferences } from '../common/s import { terminalsPath } from '../common/terminal-protocol'; import { IBaseTerminalServer, TerminalProcessInfo } from '../common/base-terminal-protocol'; import { TerminalWatcher } from '../common/terminal-watcher'; -import { TerminalWidgetOptions, TerminalWidget, TerminalDimensions, TerminalExitStatus } from './base/terminal-widget'; +import { TerminalWidgetOptions, TerminalWidget, TerminalDimensions, TerminalExitStatus, TerminalLocationOptions, TerminalLocation } from './base/terminal-widget'; import { Deferred } from '@theia/core/lib/common/promise-util'; import { TerminalPreferences, TerminalRendererType, isTerminalRendererType, DEFAULT_TERMINAL_RENDERER_TYPE, CursorStyle } from './terminal-preferences'; import URI from '@theia/core/lib/common/uri'; @@ -53,6 +53,7 @@ export interface TerminalContribution { export class TerminalWidgetImpl extends TerminalWidget implements StatefulWidget, ExtractableWidget { readonly isExtractable: boolean = true; secondaryWindow: Window | undefined; + location: TerminalLocationOptions; static LABEL = nls.localizeByDefault('Terminal'); @@ -128,6 +129,8 @@ export class TerminalWidgetImpl extends TerminalWidget implements StatefulWidget )); } + this.location = this.options.location || TerminalLocation.Panel; + this.title.closable = true; this.addClass('terminal-container');