提交 2dbf1703 编写于 作者: D Daniel Imms

Get link providers passing all the way through to the renderer

上级 16466627
......@@ -1103,9 +1103,9 @@ declare module 'vscode' {
/**
* 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.
* 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.
*/
tooltip?: string;
}
......
......@@ -9,7 +9,7 @@ import { ExtHostContext, ExtHostTerminalServiceShape, MainThreadTerminalServiceS
import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers';
import { URI } from 'vs/base/common/uri';
import { StopWatch } from 'vs/base/common/stopwatch';
import { ITerminalInstanceService, ITerminalService, ITerminalInstance, ITerminalBeforeHandleLinkEvent } from 'vs/workbench/contrib/terminal/browser/terminal';
import { ITerminalInstanceService, ITerminalService, ITerminalInstance, ITerminalBeforeHandleLinkEvent, ITerminalExternalLinkProvider, ITerminalLink } from 'vs/workbench/contrib/terminal/browser/terminal';
import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { TerminalDataBufferer } from 'vs/workbench/contrib/terminal/common/terminalDataBuffering';
......@@ -25,6 +25,13 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape
private readonly _terminalProcessProxies = new Map<number, ITerminalProcessExtHostProxy>();
private _dataEventTracker: TerminalDataEventTracker | undefined;
private _linkHandler: IDisposable | undefined;
/**
* A single shared terminal link provider for the exthost. When an ext registers a link
* provider, this is registered with the terminal on the renderer side and all links are
* provided through this, even from multiple ext link providers. Xterm should remove lower
* priority intersecting links itself.
*/
private _linkProvider: IDisposable | undefined;
constructor(
extHostContext: IExtHostContext,
......@@ -86,6 +93,8 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape
public dispose(): void {
this._toDispose.dispose();
this._linkHandler?.dispose();
this._linkProvider?.dispose();
// TODO@Daniel: Should all the previously created terminals be disposed
// when the extension host process goes down ?
......@@ -162,6 +171,18 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape
public $stopHandlingLinks(): void {
this._linkHandler?.dispose();
this._linkHandler = undefined;
}
public $startLinkProvider(): void {
this._linkProvider?.dispose();
// TODO: Verify sharing a link provider works fine with removal of intersecting links
this._linkProvider = this._terminalService.registerLinkProvider(new ExtensionTerminalLinkProvider(this._proxy));
}
public $stopLinkProvider(): void {
this._linkProvider?.dispose();
this._linkProvider = undefined;
}
private async _handleLink(e: ITerminalBeforeHandleLinkEvent): Promise<boolean> {
......@@ -395,3 +416,23 @@ class TerminalDataEventTracker extends Disposable {
this._register(this._bufferer.startBuffering(instance.id, instance.onData));
}
}
class ExtensionTerminalLinkProvider implements ITerminalExternalLinkProvider {
constructor(
private readonly _proxy: ExtHostTerminalServiceShape
) {
}
async provideLinks(instance: ITerminalInstance, line: string): Promise<ITerminalLink[] | undefined> {
const extHostLinks = await this._proxy.$provideLinks(instance.id, line);
return extHostLinks.map(dto => ({
startIndex: dto.startIndex,
length: dto.length,
label: dto.label,
activate(text: string) {
// TODO: Activate on the exthost
console.log('Activated! ' + text);
}
}));
}
}
......@@ -450,6 +450,8 @@ export interface MainThreadTerminalServiceShape extends IDisposable {
$stopSendingDataEvents(): void;
$startHandlingLinks(): void;
$stopHandlingLinks(): void;
$startLinkProvider(): void;
$stopLinkProvider(): void;
$setEnvironmentVariableCollection(extensionIdentifier: string, persistent: boolean, collection: ISerializableEnvironmentVariableCollection | undefined): void;
// Process
......@@ -1376,6 +1378,15 @@ export interface IShellAndArgsDto {
args: string[] | string | undefined;
}
export interface ITerminalLinkDto {
/** The startIndex of the link in the line. */
startIndex: number;
/** The length of the link in the line. */
length: number;
/** The descriptive label for what the link does when activated. */
label?: string;
}
export interface ITerminalDimensionsDto {
columns: number;
rows: number;
......@@ -1402,6 +1413,7 @@ export interface ExtHostTerminalServiceShape {
$getAvailableShells(): Promise<IShellDefinitionDto[]>;
$getDefaultShellAndArgs(useAutomationShell: boolean): Promise<IShellAndArgsDto>;
$handleLink(id: number, link: string): Promise<boolean>;
$provideLinks(id: number, line: string): Promise<ITerminalLinkDto[]>;
$initEnvironmentVariableCollections(collections: [string, ISerializableEnvironmentVariableCollection][]): void;
}
......
......@@ -5,7 +5,7 @@
import type * as vscode from 'vscode';
import { Event, Emitter } from 'vs/base/common/event';
import { ExtHostTerminalServiceShape, MainContext, MainThreadTerminalServiceShape, IShellLaunchConfigDto, IShellDefinitionDto, IShellAndArgsDto, ITerminalDimensionsDto } from 'vs/workbench/api/common/extHost.protocol';
import { ExtHostTerminalServiceShape, MainContext, MainThreadTerminalServiceShape, IShellLaunchConfigDto, IShellDefinitionDto, IShellAndArgsDto, ITerminalDimensionsDto, ITerminalLinkDto } from 'vs/workbench/api/common/extHost.protocol';
import { ExtHostConfigProvider } from 'vs/workbench/api/common/extHostConfiguration';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { URI, UriComponents } from 'vs/base/common/uri';
......@@ -333,6 +333,23 @@ export abstract class BaseExtHostTerminalService implements IExtHostTerminalServ
onFirstListenerAdd: () => this._proxy.$startSendingDataEvents(),
onLastListenerRemove: () => this._proxy.$stopSendingDataEvents()
});
this.registerLinkProvider({
provideTerminalLinks(ctx) {
const links: vscode.TerminalLink[] = [
{
startIndex: 0,
length: 10,
tooltip: `Open this custom "${ctx.line.substr(0, 10)}" link`
}
];
return links;
},
handleTerminalLink(link) {
// TODO: Pass provider ID back to ext host so it can trigger activate/handle
return false;
}
});
}
public abstract createTerminal(name?: string, shellPath?: string, shellArgs?: string[] | string): vscode.Terminal;
......@@ -562,13 +579,13 @@ export abstract class BaseExtHostTerminalService implements IExtHostTerminalServ
public registerLinkProvider(provider: vscode.TerminalLinkProvider): vscode.Disposable {
this._linkProviders.add(provider);
if (this._linkProviders.size === 1 && this._linkHandlers.size === 0) {
this._proxy.$startHandlingLinks();
if (this._linkProviders.size === 1) {
this._proxy.$startLinkProvider();
}
return new VSCodeDisposable(() => {
this._linkProviders.delete(provider);
if (this._linkProviders.size === 0 && this._linkHandlers.size === 0) {
this._proxy.$stopHandlingLinks();
if (this._linkProviders.size === 0) {
this._proxy.$stopLinkProvider();
}
});
}
......@@ -592,6 +609,35 @@ export abstract class BaseExtHostTerminalService implements IExtHostTerminalServ
return false;
}
public async $provideLinks(id: number, line: string): Promise<ITerminalLinkDto[]> {
const terminal = this._getTerminalById(id);
if (!terminal) {
return [];
}
// TODO: Store link activate callback
// TODO: Discard of links when appropriate
const result: ITerminalLinkDto[] = [];
const context: vscode.TerminalLinkContext = { terminal, line };
const promises: vscode.ProviderResult<vscode.TerminalLink[]>[] = [];
for (const provider of this._linkProviders) {
promises.push(provider.provideTerminalLinks(context));
}
const allProviderLinks = await Promise.all(promises);
for (const providerLinks of allProviderLinks) {
if (providerLinks && providerLinks.length > 0) {
result.push(...providerLinks.map(l => ({
startIndex: l.startIndex,
length: l.length,
label: l.tooltip
})));
}
}
return result;
}
private _onProcessExit(id: number, exitCode: number | undefined): void {
this._bufferer.stopBuffering(id);
......
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Terminal, IViewportRange, IBufferLine } from 'xterm';
import { getXtermLineContent, convertLinkRangeToBuffer } from 'vs/workbench/contrib/terminal/browser/links/terminalLinkHelpers';
import { TerminalLink } from 'vs/workbench/contrib/terminal/browser/links/terminalLink';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { TerminalBaseLinkProvider } from 'vs/workbench/contrib/terminal/browser/links/terminalBaseLinkProvider';
import { ITerminalExternalLinkProvider, ITerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminal';
export class TerminalExternalLinkProviderAdapter extends TerminalBaseLinkProvider {
constructor(
private readonly _xterm: Terminal,
private readonly _instance: ITerminalInstance,
private readonly _externalLinkProvider: ITerminalExternalLinkProvider,
private readonly _activateCallback: (event: MouseEvent | undefined, uri: string) => void,
private readonly _tooltipCallback: (link: TerminalLink, viewportRange: IViewportRange, modifierDownCallback?: () => void, modifierUpCallback?: () => void) => void,
@IInstantiationService private readonly _instantiationService: IInstantiationService
) {
super();
}
protected async _provideLinks(y: number): Promise<TerminalLink[]> {
// TODO: Need to return async
let startLine = y - 1;
let endLine = startLine;
const lines: IBufferLine[] = [
this._xterm.buffer.active.getLine(startLine)!
];
while (this._xterm.buffer.active.getLine(startLine)?.isWrapped) {
lines.unshift(this._xterm.buffer.active.getLine(startLine - 1)!);
startLine--;
}
while (this._xterm.buffer.active.getLine(endLine + 1)?.isWrapped) {
lines.push(this._xterm.buffer.active.getLine(endLine + 1)!);
endLine++;
}
const lineContent = getXtermLineContent(this._xterm.buffer.active, startLine, endLine, this._xterm.cols);
const externalLinks = await this._externalLinkProvider.provideLinks(this._instance, lineContent);
if (!externalLinks) {
return [];
}
// TODO: Add handling default handling of links via the target property
return externalLinks.map(link => {
const bufferRange = convertLinkRangeToBuffer(lines, this._xterm.cols, {
startColumn: link.startIndex + 1,
startLineNumber: 1,
endColumn: link.startIndex + link.length + 1,
endLineNumber: 1
}, startLine);
const matchingText = lineContent.substr(link.startIndex, link.length);
return this._instantiationService.createInstance(TerminalLink, bufferRange, matchingText, this._xterm.buffer.active.viewportY, this._activateCallback, this._tooltipCallback, true, link.label);
});
}
}
......@@ -144,6 +144,14 @@ export interface ITerminalService {
*/
addLinkHandler(key: string, callback: TerminalLinkHandlerCallback): IDisposable;
/**
* Registers a link provider that enables integrators to add links to the terminal.
* @param linkProvider When registered, the link provider is asked whenever a cell is hovered
* for links at that position. This lets the terminal know all links at a given area and also
* labels for what these links are going to do.
*/
registerLinkProvider(linkProvider: ITerminalExternalLinkProvider): IDisposable;
selectDefaultShell(): Promise<void>;
setContainers(panelContainer: HTMLElement, terminalContainer: HTMLElement): void;
......@@ -165,22 +173,37 @@ export interface ITerminalService {
requestStartExtensionTerminal(proxy: ITerminalProcessExtHostProxy, cols: number, rows: number): Promise<ITerminalLaunchError | undefined>;
}
export interface ISearchOptions {
/**
* Similar to xterm.js' ILinkProvider but using promises and hides xterm.js internals (like buffer
* positions, decorations, etc.) from the rest of vscode. This is the interface to use for
* workbench integrations.
*/
export interface ITerminalExternalLinkProvider {
provideLinks(instance: ITerminalInstance, line: string): Promise<ITerminalLink[] | undefined>;
}
export interface ITerminalLink {
/** The startIndex of the link in the line. */
startIndex: number;
/** The length of the link in the line. */
length: number;
/** The descriptive label for what the link does when activated. */
label?: string;
/**
* Whether the find should be done as a regex.
* Activates the link.
* @param text The text of the link.
*/
activate(text: string): void;
}
export interface ISearchOptions {
/** Whether the find should be done as a regex. */
regex?: boolean;
/**
* Whether only whole words should match.
*/
/** Whether only whole words should match. */
wholeWord?: boolean;
/**
* Whether find should pay attention to case.
*/
/** Whether find should pay attention to case. */
caseSensitive?: boolean;
/**
* Whether the search should start at the current search position (not the next row)
*/
/** Whether the search should start at the current search position (not the next row). */
incremental?: boolean;
}
......@@ -272,8 +295,15 @@ export interface ITerminalInstance {
readonly exitCode: number | undefined;
/** A promise that resolves when the terminal's pty/process have been created. */
processReady: Promise<void>;
/** Whether xterm.js has been created. */
isXtermReady: boolean;
/** A promise that resolves when xterm.js has been created. */
xtermReady: Promise<void>;
/**
* The title of the terminal. This is either title or the process currently running or an
* explicit name given to the terminal instance through the extension API.
......@@ -480,4 +510,10 @@ export interface ITerminalInstance {
getInitialCwd(): Promise<string>;
getCwd(): Promise<string>;
// TODO: Improve naming of ITerminalLinkProvider and ITerminalLink, they conflict with the wrappers that are applied internally (TerminalBaseLinkProvider)
/**
* @throws when called before xterm.js is ready.
*/
registerLinkProvider(provider: ITerminalExternalLinkProvider): IDisposable;
}
......@@ -30,7 +30,7 @@ import { ansiColorIdentifiers, TERMINAL_BACKGROUND_COLOR, TERMINAL_CURSOR_BACKGR
import { TerminalConfigHelper } from 'vs/workbench/contrib/terminal/browser/terminalConfigHelper';
import { TerminalLinkManager } from 'vs/workbench/contrib/terminal/browser/links/terminalLinkManager';
import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility';
import { ITerminalInstanceService, ITerminalInstance, TerminalShellType, WindowsShellType, ITerminalBeforeHandleLinkEvent } from 'vs/workbench/contrib/terminal/browser/terminal';
import { ITerminalInstanceService, ITerminalInstance, TerminalShellType, WindowsShellType, ITerminalBeforeHandleLinkEvent, ITerminalExternalLinkProvider } from 'vs/workbench/contrib/terminal/browser/terminal';
import { TerminalProcessManager } from 'vs/workbench/contrib/terminal/browser/terminalProcessManager';
import { Terminal as XTermTerminal, IBuffer, ITerminalAddon } from 'xterm';
import { SearchAddon, ISearchOptions } from 'xterm-addon-search';
......@@ -127,6 +127,8 @@ export class TerminalInstance extends Disposable implements ITerminalInstance {
// TODO: How does this work with detached processes?
// TODO: Should this be an event as it can fire twice?
public get processReady(): Promise<void> { return this._processManager.ptyProcessReady; }
public get isXtermReady(): boolean { return !!this._xterm; }
public get xtermReady(): Promise<void> { return this._xtermReadyPromise.then(() => { }); }
public get exitCode(): number | undefined { return this._exitCode; }
public get title(): string { return this._title; }
public get hadFocusOnExit(): boolean { return this._hadFocusOnExit; }
......@@ -1492,6 +1494,41 @@ export class TerminalInstance extends Disposable implements ITerminalInstance {
public getCwd(): Promise<string> {
return this._processManager.getCwd();
}
public registerLinkProvider(provider: ITerminalExternalLinkProvider): IDisposable {
if (!this._xterm) {
throw new Error('TerminalInstance.registerLinkProvider before xterm was created');
}
// TODO: Convert into TerminalBaseLinkProvider
const xterm = this._xterm;
const instance = this;
console.log('TerminalInstance.registerLinkProvider');
return xterm.registerLinkProvider({
async provideLinks(bufferLineNumber, callback) {
console.log('TerminalInstance provideLinks');
// TODO: Get and handle the full wrapped line
const bufferLine = xterm.buffer.active.getLine(bufferLineNumber - 1);
if (!bufferLine) {
return callback(undefined);
}
const lineString = bufferLine.translateToString();
const lineLinks = await provider.provideLinks(instance, lineString);
if (!lineLinks) {
return callback(undefined);
}
console.log(' result', lineLinks);
callback(lineLinks.map(l => ({
range: {
start: { x: l.startIndex, y: bufferLineNumber },
end: { x: l.startIndex + l.length, y: bufferLineNumber }
},
text: lineString.substr(l.startIndex, l.length),
activate: (_, text) => console.log('Activated! ' + text)
})));
}
});
}
}
registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => {
......
......@@ -14,7 +14,7 @@ import { TerminalTab } from 'vs/workbench/contrib/terminal/browser/terminalTab';
import { IInstantiationService, optional } from 'vs/platform/instantiation/common/instantiation';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import { TerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminalInstance';
import { ITerminalService, ITerminalInstance, ITerminalTab, TerminalShellType, WindowsShellType, TerminalLinkHandlerCallback, LINK_INTERCEPT_THRESHOLD } from 'vs/workbench/contrib/terminal/browser/terminal';
import { ITerminalService, ITerminalInstance, ITerminalTab, TerminalShellType, WindowsShellType, TerminalLinkHandlerCallback, LINK_INTERCEPT_THRESHOLD, ITerminalExternalLinkProvider } from 'vs/workbench/contrib/terminal/browser/terminal';
import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService';
import { TerminalConfigHelper } from 'vs/workbench/contrib/terminal/browser/terminalConfigHelper';
import { IQuickInputService, IQuickPickItem, IPickOptions } from 'vs/platform/quickinput/common/quickInput';
......@@ -54,6 +54,7 @@ export class TerminalService implements ITerminalService {
private _extHostsReady: { [authority: string]: IExtHostReadyEntry | undefined } = {};
private _activeTabIndex: number;
private _linkHandlers: { [key: string]: TerminalLinkHandlerCallback } = {};
private _linkProviders: Set<ITerminalExternalLinkProvider> = new Set();
public get activeTabIndex(): number { return this._activeTabIndex; }
public get terminalInstances(): ITerminalInstance[] { return this._terminalInstances; }
......@@ -84,6 +85,8 @@ export class TerminalService implements ITerminalService {
public get onInstancesChanged(): Event<void> { return this._onInstancesChanged.event; }
private readonly _onInstanceTitleChanged = new Emitter<ITerminalInstance>();
public get onInstanceTitleChanged(): Event<ITerminalInstance> { return this._onInstanceTitleChanged.event; }
private readonly _onInstanceXtermReady = new Emitter<ITerminalInstance>();
public get onInstanceXtermReady(): Event<ITerminalInstance> { return this._onInstanceXtermReady.event; }
private readonly _onActiveInstanceChanged = new Emitter<ITerminalInstance | undefined>();
public get onActiveInstanceChanged(): Event<ITerminalInstance | undefined> { return this._onActiveInstanceChanged.event; }
private readonly _onTabDisposed = new Emitter<ITerminalTab>();
......@@ -128,6 +131,7 @@ export class TerminalService implements ITerminalService {
const instance = this.getActiveInstance();
this._onActiveInstanceChanged.fire(instance ? instance : undefined);
});
this.onInstanceXtermReady(instance => this._setInstanceLinkProviders(instance));
this._handleContextKeys();
}
......@@ -478,6 +482,35 @@ export class TerminalService implements ITerminalService {
};
}
public registerLinkProvider(linkProvider: ITerminalExternalLinkProvider): IDisposable {
// TODO: Register it from the main thread class
const disposables: IDisposable[] = [];
this._linkProviders.add(linkProvider);
for (const instance of this.terminalInstances) {
// Only register immediately when xterm is ready
if (instance.isXtermReady) {
disposables.push(instance.registerLinkProvider(linkProvider));
}
}
console.log('registerLinkProvider register');
return {
dispose: () => {
console.log('registerLinkProvider dispose');
// TODO: Remove from xterm instances
for (const disposable of disposables) {
disposable.dispose();
}
this._linkProviders.delete(linkProvider);
}
};
}
private _setInstanceLinkProviders(instance: ITerminalInstance): void {
for (const linkProvider of this._linkProviders) {
instance.registerLinkProvider(linkProvider);
}
}
private _getTabForInstance(instance: ITerminalInstance): ITerminalTab | undefined {
return find(this._terminalTabs, tab => tab.terminalInstances.indexOf(instance) !== -1);
}
......@@ -628,6 +661,7 @@ export class TerminalService implements ITerminalService {
public createInstance(container: HTMLElement | undefined, shellLaunchConfig: IShellLaunchConfig): ITerminalInstance {
const instance = this._instantiationService.createInstance(TerminalInstance, this._terminalFocusContextKey, this._terminalShellTypeContextKey, this._configHelper, container, shellLaunchConfig);
this._onInstanceCreated.fire(instance);
instance.xtermReady.then(() => this._onInstanceXtermReady.fire(instance));
return instance;
}
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册