diff --git a/packages/core/src/common/index.ts b/packages/core/src/common/index.ts index cedfd254d0009..257f263cc01bf 100644 --- a/packages/core/src/common/index.ts +++ b/packages/core/src/common/index.ts @@ -43,6 +43,7 @@ export * from './contribution-filter'; export * from './nls'; export * from './numbers'; export * from './performance'; +export * from './view-column'; import { environment } from '@theia/application-package/lib/environment'; export { environment }; diff --git a/packages/core/src/common/view-column.ts b/packages/core/src/common/view-column.ts new file mode 100644 index 0000000000000..0548370518db3 --- /dev/null +++ b/packages/core/src/common/view-column.ts @@ -0,0 +1,33 @@ +// ***************************************************************************** +// Copyright (C) 2022 STMicroelectronics. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 +// ***************************************************************************** + +/** + * Denotes a column in the editor window. + * Columns are used to show editors side by side. + */ +export enum ViewColumn { + Active = -1, + Beside = -2, + One = 1, + Two = 2, + Three = 3, + Four = 4, + Five = 5, + Six = 6, + Seven = 7, + Eight = 8, + Nine = 9 +} diff --git a/packages/plugin-ext/src/common/plugin-api-rpc.ts b/packages/plugin-ext/src/common/plugin-api-rpc.ts index 024a7d9247b74..02e68b2b8f7f8 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 5a5cb05d1618c..54e2edf88453d 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,23 @@ export class TerminalServiceMainImpl implements TerminalServiceMain, TerminalLin } } + protected getTerminalLocation(options: TerminalOptions, parentId?: string): TerminalLocation | TerminalEditorLocationOptions | { parentTerminal: string; } | undefined { + if (typeof options.location === 'number' && Object.values(TerminalLocation).includes(options.location)) { + 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'); + } + 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 2eaec99943d93..3c5a85b728c69 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, @@ -1088,7 +1089,7 @@ export function createAPIFactory( notebook: theia.NotebookDocument, controller: theia.NotebookController ): (void | Thenable) { }, - onDidChangeSelectedNotebooks: () => Disposable.create(() => {}), + onDidChangeSelectedNotebooks: () => Disposable.create(() => { }), updateNotebookAffinity: (notebook: theia.NotebookDocument, affinity: theia.NotebookControllerAffinity) => undefined, dispose: () => undefined, }; @@ -1099,7 +1100,7 @@ export function createAPIFactory( ) { return { rendererId, - onDidReceiveMessage: () => Disposable.create(() => {} ), + onDidReceiveMessage: () => Disposable.create(() => { }), postMessage: () => Promise.resolve({}), }; }, @@ -1284,6 +1285,7 @@ export function createAPIFactory( TabInputNotebookDiff: NotebookDiffEditorTabInput, TabInputWebview: WebviewEditorTabInput, TabInputTerminal: TerminalEditorTabInput, + 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 647117dabf8d2..7a3320b35ddab 100644 --- a/packages/plugin-ext/src/plugin/types-impl.ts +++ b/packages/plugin-ext/src/plugin/types-impl.ts @@ -1910,6 +1910,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 a0ad3e1771db2..284ca04d1da3e 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..e355f8312e6ae 100644 --- a/packages/terminal/src/browser/base/terminal-widget.ts +++ b/packages/terminal/src/browser/base/terminal-widget.ts @@ -14,7 +14,7 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 // ***************************************************************************** -import { Event } from '@theia/core'; +import { Event, ViewColumn } from '@theia/core'; import { BaseWidget } from '@theia/core/lib/browser'; import { CommandLineOptions } from '@theia/process/lib/common/shell-command-builder'; import { TerminalSearchWidget } from '../search/terminal-search-widget'; @@ -30,13 +30,28 @@ 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: ViewColumn; + readonly preserveFocus?: boolean; +} + +export interface TerminalSplitLocation { + readonly parentTerminal: string; +} + /** * Terminal UI widget. */ export abstract class TerminalWidget extends BaseWidget { abstract processId: Promise; - /** * Get the current executable and arguments. */ @@ -54,6 +69,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 +229,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..60fbf57c2b52b 100644 --- a/packages/terminal/src/browser/terminal-frontend-contribution.ts +++ b/packages/terminal/src/browser/terminal-frontend-contribution.ts @@ -25,7 +25,8 @@ import { isOSX, SelectionService, Emitter, - Event + Event, + ViewColumn } from '@theia/core/lib/common'; import { ApplicationShell, KeybindingContribution, KeyCode, Key, WidgetManager, @@ -36,7 +37,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 } 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 +645,46 @@ 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?.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-after'; + break; + case ViewColumn.Beside: + widgetOptions.ref = this.shell.currentWidget; + widgetOptions.mode = 'split-right'; + break; + default: + widgetOptions.area = 'main'; + const mainAreaTerminals = this.shell.getWidgets('main').filter(w => w instanceof TerminalWidget && w.isVisible); + const column = Math.min(widget.location.viewColumn, mainAreaTerminals.length); + widgetOptions.mode = widget.location.viewColumn <= mainAreaTerminals.length ? 'split-left' : 'split-right'; + widgetOptions.ref = mainAreaTerminals[column - 1]; + } + } + } + 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');