Skip to content

Commit

Permalink
terminal: Support VS Code Terminal Link Providers (#11552)
Browse files Browse the repository at this point in the history
* Implement the previously mocked support for Terminal Link Providers
* Introduce Theia contribution point for adding link providers
* Migrate existing link matchers to this new contribution point
* Get rid of the usage of the deprecated xterm.registerLinkMatcher API
* Fix UI bug where the link hover doesn't show up
  This happened when the mouse is above the hover as it gets
  visible, leading to xterm canceling the hover right away, because the
  mouse "seemingly" left the link.
  This has been fixed by preventing to hide it   if the mouse left the
  link but is above the hover.
* Turn the hover text into a clickable link

Contributed on behalf of STMicroelectronics.

Fixes #11521
Fixes #11507
Fixes #11491

Change-Id: I01f907d778f4a5f0588202ea28c4c82252ab75dc
Signed-off-by: Philip Langer <planger@eclipsesource.com>
  • Loading branch information
planger authored Oct 12, 2022
1 parent 807cf6d commit cac96ae
Show file tree
Hide file tree
Showing 16 changed files with 758 additions and 272 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ generating the main code bundle (as before), the second serves to generate a *.c
- [core] changed type of `FrontendApplicationConfig#defaultTheme` from `string` to `DefaultTheme` [#11570](https://github.com/eclipse-theia/theia/pull/11570)
- From now on, the default theme can be dispatched based on the OS theme. Use `DefaultTheme#defaultForOSTheme` to derive the `string` theme ID.
- [plugin-ext] removed `ctrlcmd+shift+l` keybinding for `pluginsView:toggle` [#11608](https://github.com/eclipse-theia/theia/pull/11608)
- [terminal] The `AbstractCmdClickTerminalContribution` API has been removed in favor of the `TerminalLinkProvider` interface [#11552](https://github.com/eclipse-theia/theia/pull/11552) - Contributed on behalf of STMicroelectronics

## v1.29.0 - 8/25/2022

Expand Down
4 changes: 4 additions & 0 deletions packages/plugin-ext/src/common/plugin-api-rpc-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -723,3 +723,7 @@ export interface CommentInfo {
threads: CommentThread[];
commentingRanges: CommentingRanges;
}

export interface ProvidedTerminalLink extends theia.TerminalLink {
providerId: string
}
17 changes: 16 additions & 1 deletion packages/plugin-ext/src/common/plugin-api-rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,8 @@ import {
CommentThread,
CommentThreadChangedEvent,
CodeActionProviderDocumentation,
LinkedEditingRanges
LinkedEditingRanges,
ProvidedTerminalLink
} from './plugin-api-rpc-model';
import { ExtPluginApi } from './plugin-ext-api-contribution';
import { KeysToAnyValues, KeysToKeysToAnyValue } from './types';
Expand Down Expand Up @@ -269,6 +270,8 @@ export interface TerminalServiceExt {
$currentTerminalChanged(id: string | undefined): void;
$terminalStateChanged(id: string): void;
$initEnvironmentVariableCollections(collections: [string, SerializableEnvironmentVariableCollection][]): void;
$provideTerminalLinks(line: string, terminalId: string, token: theia.CancellationToken): Promise<ProvidedTerminalLink[]>;
$handleTerminalLink(link: ProvidedTerminalLink): Promise<void>;
getEnvironmentVariableCollection(extensionIdentifier: string): theia.EnvironmentVariableCollection;
}
export interface OutputChannelRegistryExt {
Expand Down Expand Up @@ -394,6 +397,18 @@ export interface TerminalServiceMain {
* @param name new terminal widget name.
*/
$setNameByTerminalId(id: number, name: string): void;

/**
* Register a new terminal link provider.
* @param providerId id of the terminal link provider to be registered.
*/
$registerTerminalLinkProvider(providerId: string): Promise<void>;

/**
* Unregister the terminal link provider with the specified id.
* @param providerId id of the terminal link provider to be unregistered.
*/
$unregisterTerminalLinkProvider(providerId: string): Promise<void>;
}

export interface AutoFocus {
Expand Down
27 changes: 26 additions & 1 deletion packages/plugin-ext/src/main/browser/terminal-main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,24 +17,27 @@
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 { 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';
import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
import { SerializableEnvironmentVariableCollection } from '@theia/terminal/lib/common/base-terminal-protocol';
import { ShellTerminalServerProxy } from '@theia/terminal/lib/common/shell-terminal-protocol';
import { TerminalLink, TerminalLinkProvider } from '@theia/terminal/lib/browser/terminal-link-provider';
import { URI } from '@theia/core/lib/common/uri';

/**
* Plugin api service allows working with terminal emulator.
*/
export class TerminalServiceMainImpl implements TerminalServiceMain, Disposable {
export class TerminalServiceMainImpl implements TerminalServiceMain, TerminalLinkProvider, Disposable {

private readonly terminals: TerminalService;
private readonly shell: ApplicationShell;
private readonly extProxy: TerminalServiceExt;
private readonly shellTerminalServer: ShellTerminalServerProxy;
private readonly terminalLinkProviders: string[] = [];

private readonly toDispose = new DisposableCollection();

Expand All @@ -54,6 +57,8 @@ export class TerminalServiceMainImpl implements TerminalServiceMain, Disposable
const serializedCollections: [string, SerializableEnvironmentVariableCollection][] = collectionAsArray.map(e => [e[0], [...e[1].map.entries()]]);
this.extProxy.$initEnvironmentVariableCollections(serializedCollections);
}

container.bind(TerminalLinkProvider).toDynamicValue(() => this);
}

$setEnvironmentVariableCollection(extensionIdentifier: string, persistent: boolean, collection: SerializableEnvironmentVariableCollection | undefined): void {
Expand Down Expand Up @@ -242,4 +247,24 @@ export class TerminalServiceMainImpl implements TerminalServiceMain, Disposable
$setNameByTerminalId(id: number, name: string): void {
this.terminals.getByTerminalId(id)?.setTitle(name);
}

async $registerTerminalLinkProvider(providerId: string): Promise<void> {
this.terminalLinkProviders.push(providerId);
}

async $unregisterTerminalLinkProvider(providerId: string): Promise<void> {
const index = this.terminalLinkProviders.indexOf(providerId);
if (index > -1) {
this.terminalLinkProviders.splice(index, 1);
}
}

async provideLinks(line: string, terminal: TerminalWidget, cancelationToken?: CancellationToken | undefined): Promise<TerminalLink[]> {
if (this.terminalLinkProviders.length < 1) {
return [];
}
const links = await this.extProxy.$provideTerminalLinks(line, terminal.id, cancelationToken ?? CancellationToken.None);
return links.map(link => ({ ...link, handle: () => this.extProxy.$handleTerminalLink(link) }));
}

}
8 changes: 5 additions & 3 deletions packages/plugin-ext/src/plugin/plugin-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,8 @@ import {
LinkedEditingRanges,
LanguageStatusSeverity,
TextDocumentChangeReason,
InputBoxValidationSeverity
InputBoxValidationSeverity,
TerminalLink
} from './types-impl';
import { AuthenticationExtImpl } from './authentication-ext';
import { SymbolKind } from '../common/plugin-api-rpc-model';
Expand Down Expand Up @@ -471,8 +472,8 @@ export function createAPIFactory(
createInputBox(): theia.InputBox {
return quickOpenExt.createInputBox(plugin);
},
registerTerminalLinkProvider(provider: theia.TerminalLinkProvider): void {
/* NOOP. To be implemented at later stage */
registerTerminalLinkProvider(provider: theia.TerminalLinkProvider): theia.Disposable {
return terminalExt.registerTerminalLinkProvider(provider);
},
get activeColorTheme(): theia.ColorTheme {
return themingExt.activeColorTheme;
Expand Down Expand Up @@ -1036,6 +1037,7 @@ export function createAPIFactory(
ColorThemeKind,
SourceControlInputBoxValidationType,
FileDecoration,
TerminalLink,
CancellationError,
ExtensionMode,
LinkedEditingRanges,
Expand Down
38 changes: 37 additions & 1 deletion packages/plugin-ext/src/plugin/terminal-ext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@ import { RPCProtocol } from '../common/rpc-protocol';
import { Event, Emitter } from '@theia/core/lib/common/event';
import { Deferred } from '@theia/core/lib/common/promise-util';
import * as theia from '@theia/plugin';
import { EnvironmentVariableMutatorType } from './types-impl';
import { Disposable, EnvironmentVariableMutatorType } from './types-impl';
import { SerializableEnvironmentVariableCollection } from '@theia/terminal/lib/common/base-terminal-protocol';
import { ProvidedTerminalLink } from '../common/plugin-api-rpc-model';

/**
* Provides high level terminal plugin api to use in the Theia plugins.
Expand All @@ -35,6 +36,9 @@ export class TerminalServiceExtImpl implements TerminalServiceExt {

private readonly _pseudoTerminals = new Map<string, PseudoTerminal>();

private static nextTerminalLinkProviderId = 0;
private readonly terminalLinkProviders = new Map<string, theia.TerminalLinkProvider>();

private readonly onDidCloseTerminalEmitter = new Emitter<Terminal>();
readonly onDidCloseTerminal: theia.Event<Terminal> = this.onDidCloseTerminalEmitter.event;

Expand Down Expand Up @@ -181,6 +185,38 @@ export class TerminalServiceExtImpl implements TerminalServiceExt {
this.onDidChangeActiveTerminalEmitter.fire(this.activeTerminal);
}

registerTerminalLinkProvider(provider: theia.TerminalLinkProvider): theia.Disposable {
const providerId = (TerminalServiceExtImpl.nextTerminalLinkProviderId++).toString();
this.terminalLinkProviders.set(providerId, provider);
this.proxy.$registerTerminalLinkProvider(providerId);
return Disposable.create(() => {
this.proxy.$unregisterTerminalLinkProvider(providerId);
this.terminalLinkProviders.delete(providerId);
});
}

async $provideTerminalLinks(line: string, terminalId: string, token: theia.CancellationToken): Promise<ProvidedTerminalLink[]> {
const links: ProvidedTerminalLink[] = [];
const terminal = this._terminals.get(terminalId);
if (terminal) {
for (const [providerId, provider] of this.terminalLinkProviders) {
const providedLinks = await provider.provideTerminalLinks({ line, terminal }, token);
if (providedLinks) {
links.push(...providedLinks.map(link => ({ ...link, providerId })));
}
}
}
return links;
}

async $handleTerminalLink(link: ProvidedTerminalLink): Promise<void> {
const provider = this.terminalLinkProviders.get(link.providerId);
if (!provider) {
throw Error('Terminal link provider not found');
}
await provider.handleTerminalLink(link);
}

/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
Expand Down
23 changes: 23 additions & 0 deletions packages/plugin-ext/src/plugin/types-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1649,6 +1649,29 @@ export class QuickInputButtons {
};
}

@es5ClassCompat
export class TerminalLink {

static validate(candidate: TerminalLink): void {
if (typeof candidate.startIndex !== 'number') {
throw new Error('Should provide a startIndex inside candidate field');
}
if (typeof candidate.length !== 'number') {
throw new Error('Should provide a length inside candidate field');
}
}

startIndex: number;
length: number;
tooltip?: string;

constructor(startIndex: number, length: number, tooltip?: string) {
this.startIndex = startIndex;
this.length = length;
this.tooltip = tooltip;
}
}

@es5ClassCompat
export class FileDecoration {

Expand Down
16 changes: 14 additions & 2 deletions packages/plugin/src/theia.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3274,7 +3274,7 @@ export module '@theia/plugin' {
/**
* A link on a terminal line.
*/
export interface TerminalLink {
export class TerminalLink {
/**
* The start index of the link on [TerminalLinkContext.line](#TerminalLinkContext.line].
*/
Expand All @@ -3293,6 +3293,18 @@ export module '@theia/plugin' {
* depending on OS, user settings, and localization.
*/
tooltip?: string;

/**
* Creates a new terminal link.
* @param startIndex The start index of the link on [TerminalLinkContext.line](#TerminalLinkContext.line].
* @param length The length of the link on [TerminalLinkContext.line](#TerminalLinkContext.line].
* @param tooltip The tooltip text when you hover over this link.
*
* If a tooltip is provided, is will be displayed in a string that includes instructions on
* how to trigger the link, such as `{0} (ctrl + click)`. The specific instructions vary
* depending on OS, user settings, and localization.
*/
constructor(startIndex: number, length: number, tooltip?: string);
}

/**
Expand Down Expand Up @@ -5124,7 +5136,7 @@ export module '@theia/plugin' {
* @param provider The provider that provides the terminal links.
* @return Disposable that unregisters the provider.
*/
export function registerTerminalLinkProvider(provider: TerminalLinkProvider): void;
export function registerTerminalLinkProvider(provider: TerminalLinkProvider): Disposable;

/**
* Register a file decoration provider.
Expand Down
Loading

0 comments on commit cac96ae

Please sign in to comment.