Skip to content

Commit

Permalink
terminal: Support VS Code Terminal Link Providers
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 <[email protected]>
  • Loading branch information
planger committed Sep 7, 2022
1 parent 98a0bf6 commit a7992c4
Show file tree
Hide file tree
Showing 15 changed files with 757 additions and 272 deletions.
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 @@ -685,3 +685,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 @@ -74,7 +74,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 @@ -266,6 +267,8 @@ export interface TerminalServiceExt {
$terminalSizeChanged(id: string, cols: number, rows: number): void;
$currentTerminalChanged(id: string | undefined): 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 @@ -377,6 +380,18 @@ export interface TerminalServiceMain {
$disposeByTerminalId(id: number, waitOnExit?: boolean | string): void;

$setEnvironmentVariableCollection(extensionIdentifier: string, persistent: boolean, collection: SerializableEnvironmentVariableCollection | undefined): 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 @@ -232,4 +237,24 @@ export class TerminalServiceMainImpl implements TerminalServiceMain, Disposable
terminal.dispose();
}
}

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 @@ -145,7 +145,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 @@ -466,8 +467,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 @@ -1024,6 +1025,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 @@ -167,6 +171,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 @@ -1558,6 +1558,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 @@ -3147,7 +3147,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 @@ -3166,6 +3166,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 @@ -5004,7 +5016,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 a7992c4

Please sign in to comment.