Skip to content

Commit

Permalink
xtermTerminal -> linkManager (#141172)
Browse files Browse the repository at this point in the history
  • Loading branch information
meganrogge committed Jan 22, 2022
1 parent eb416b4 commit f695611
Show file tree
Hide file tree
Showing 6 changed files with 220 additions and 175 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { TerminalExternalLinkProviderAdapter } from 'vs/workbench/contrib/termin
import { ITunnelService } from 'vs/platform/tunnel/common/tunnel';
import { XtermTerminal } from 'vs/workbench/contrib/terminal/browser/xterm/xtermTerminal';
import { ITerminalCapabilityStore } from 'vs/workbench/contrib/terminal/common/capabilities/capabilities';
import { EventType } from 'vs/base/browser/dom';

export type XtermLinkMatcherHandler = (event: MouseEvent | undefined, link: string) => Promise<void>;
export type XtermLinkMatcherValidationCallback = (uri: string, callback: (isValid: boolean) => void) => void;
Expand Down Expand Up @@ -101,7 +102,46 @@ export class TerminalLinkManager extends DisposableStore {
this._registerStandardLinkProviders();
}

async getLinks(y: number): Promise<IDetectedLinks | undefined> {
async openRecentLink(type: 'file' | 'web'): Promise<ILink | undefined> {
let links;
let i = this._xtermTerminal.raw.buffer.active.length;
while ((!links || links.length === 0) && i >= this._xtermTerminal.raw.buffer.active.viewportY) {
links = await this.getLinksForType(i, type);
i--;
}

if (!links || links.length < 1) {
return undefined;
}
const event = new TerminalLinkQuickPickEvent(EventType.CLICK);
links[0].activate(event, links[0].text);
return links[0];
}

async getLinks(): Promise<IDetectedLinks> {
const wordResults: ILink[] = [];
const webResults: ILink[] = [];
const fileResults: ILink[] = [];

for (let i = this._xtermTerminal.raw.buffer.active.length - 1; i >= this._xtermTerminal.raw.buffer.active.viewportY; i--) {
const links = await this._getLinksForLine(i);
if (links) {
const { wordLinks, webLinks, fileLinks } = links;
if (wordLinks && wordLinks.length) {
wordResults.push(...wordLinks.reverse());
}
if (webLinks && webLinks.length) {
webResults.push(...webLinks.reverse());
}
if (fileLinks && fileLinks.length) {
fileResults.push(...fileLinks.reverse());
}
}
}
return { webLinks: webResults, fileLinks: fileResults, wordLinks: wordResults };
}

private async _getLinksForLine(y: number): Promise<IDetectedLinks | undefined> {
let unfilteredWordLinks = await this.getLinksForType(y, 'word');
const webLinks = await this.getLinksForType(y, 'web');
const fileLinks = await this.getLinksForType(y, 'file');
Expand All @@ -118,6 +158,7 @@ export class TerminalLinkManager extends DisposableStore {
}
return { wordLinks, webLinks, fileLinks };
}

async getLinksForType(y: number, type: 'word' | 'web' | 'file'): Promise<ILink[] | undefined> {
switch (type) {
case 'word':
Expand All @@ -128,6 +169,7 @@ export class TerminalLinkManager extends DisposableStore {
return (await new Promise<ILink[] | undefined>(r => this._standardLinkProviders.get(TerminalValidatedLocalLinkProvider.id)?.provideLinks(y, r)));
}
}

private _tooltipCallback(link: TerminalLink, viewportRange: IViewportRange, modifierDownCallback?: () => void, modifierUpCallback?: () => void) {
if (!this._widgetManager) {
return;
Expand Down Expand Up @@ -188,7 +230,7 @@ export class TerminalLinkManager extends DisposableStore {
}

registerExternalLinkProvider(instance: ITerminalInstance, linkProvider: ITerminalExternalLinkProvider): IDisposable {
// Clear and re-register the standard link providers so they are a lower priority that the new one
// Clear and re-register the standard link providers so they are a lower priority than the new one
this._clearLinkProviders();
const wrappedLinkProvider = this._instantiationService.createInstance(TerminalExternalLinkProviderAdapter, this._xterm, instance, linkProvider, this._wrapLinkHandler.bind(this), this._tooltipCallback.bind(this));
const newLinkProvider = this._xterm.registerLinkProvider(wrappedLinkProvider);
Expand Down
12 changes: 0 additions & 12 deletions src/vs/workbench/contrib/terminal/browser/terminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,7 @@ import { DeserializedTerminalEditorInput } from 'vs/workbench/contrib/terminal/b
import { TerminalEditorInput } from 'vs/workbench/contrib/terminal/browser/terminalEditorInput';
import { EditorGroupColumn } from 'vs/workbench/services/editor/common/editorGroupColumn';
import { IKeyMods } from 'vs/platform/quickinput/common/quickInput';
import { ILink } from 'xterm';
import { ITerminalCapabilityStore } from 'vs/workbench/contrib/terminal/common/capabilities/capabilities';
import { IDetectedLinks, TerminalLinkManager } from 'vs/workbench/contrib/terminal/browser/links/terminalLinkManager';

export const ITerminalService = createDecorator<ITerminalService>('terminalService');
export const ITerminalEditorService = createDecorator<ITerminalEditorService>('terminalEditorService');
Expand Down Expand Up @@ -869,16 +867,6 @@ export interface IXtermTerminal {
* viewport.
*/
clearBuffer(): void;

/*
* Activates the most recent link for the type
*/
openRecentLink(linkManager: TerminalLinkManager, type: 'file' | 'web'): Promise<ILink | undefined>;

/*
* Gets all of the links
*/
getLinks(linkManager: TerminalLinkManager): Promise<IDetectedLinks>;
}

export interface IRequestAddInstanceToGroupEvent {
Expand Down
4 changes: 2 additions & 2 deletions src/vs/workbench/contrib/terminal/browser/terminalInstance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -683,7 +683,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance {
if (!this.xterm) {
throw new Error('no xterm');
}
return this.xterm.getLinks(this._linkManager);
return this._linkManager.getLinks();
}

async openRecentLink(type: 'file' | 'web'): Promise<void> {
Expand All @@ -693,7 +693,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance {
if (!this.xterm) {
throw new Error('no xterm');
}
this.xterm.openRecentLink(this._linkManager, type);
this._linkManager.openRecentLink(type);
}

async runRecent(type: 'command' | 'cwd'): Promise<void> {
Expand Down
45 changes: 2 additions & 43 deletions src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import type { IBuffer, ILink, ITheme, RendererType, Terminal as RawXtermTerminal } from 'xterm';
import type { IBuffer, ITheme, RendererType, Terminal as RawXtermTerminal } from 'xterm';
import type { ISearchOptions, SearchAddon as SearchAddonType } from 'xterm-addon-search';
import type { Unicode11Addon as Unicode11AddonType } from 'xterm-addon-unicode11';
import type { WebglAddon as WebglAddonType } from 'xterm-addon-webgl';
Expand All @@ -15,7 +15,7 @@ import { IEditorOptions } from 'vs/editor/common/config/editorOptions';
import { TerminalLocation, TerminalSettingId } from 'vs/platform/terminal/common/terminal';
import { IShellIntegration, ITerminalFont, TERMINAL_VIEW_ID } from 'vs/workbench/contrib/terminal/common/terminal';
import { isSafari } from 'vs/base/browser/browser';
import { ICommandTracker, IXtermTerminal, TerminalLinkQuickPickEvent } from 'vs/workbench/contrib/terminal/browser/terminal';
import { ICommandTracker, IXtermTerminal } from 'vs/workbench/contrib/terminal/browser/terminal';
import { ILogService } from 'vs/platform/log/common/log';
import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
import { TerminalStorageKeys } from 'vs/workbench/contrib/terminal/common/terminalStorageKeys';
Expand All @@ -30,8 +30,6 @@ import { TERMINAL_FOREGROUND_COLOR, TERMINAL_BACKGROUND_COLOR, TERMINAL_CURSOR_F
import { Color } from 'vs/base/common/color';
import { ShellIntegrationAddon } from 'vs/workbench/contrib/terminal/browser/xterm/shellIntegrationAddon';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IDetectedLinks, TerminalLinkManager } from 'vs/workbench/contrib/terminal/browser/links/terminalLinkManager';
import { EventType } from 'vs/base/browser/dom';
import { ITerminalCapabilityStore } from 'vs/workbench/contrib/terminal/common/capabilities/capabilities';

// How long in milliseconds should an average frame take to render for a notification to appear
Expand Down Expand Up @@ -154,45 +152,6 @@ export class XtermTerminal extends DisposableStore implements IXtermTerminal {
this.raw.loadAddon(this._shellIntegrationAddon);
}

async getLinks(linkManager: TerminalLinkManager): Promise<IDetectedLinks> {
const wordResults: ILink[] = [];
const webResults: ILink[] = [];
const fileResults: ILink[] = [];

for (let i = this.raw.buffer.active.length - 1; i >= this.raw.buffer.active.viewportY; i--) {
const links = await linkManager.getLinks(i);
if (links) {
const { wordLinks, webLinks, fileLinks } = links;
if (wordLinks && wordLinks.length) {
wordResults.push(...wordLinks.reverse());
}
if (webLinks && webLinks.length) {
webResults.push(...webLinks.reverse());
}
if (fileLinks && fileLinks.length) {
fileResults.push(...fileLinks.reverse());
}
}
}
return { webLinks: webResults, fileLinks: fileResults, wordLinks: wordResults };
}

async openRecentLink(linkManager: TerminalLinkManager, type: 'file' | 'web'): Promise<ILink | undefined> {
let links;
let i = this.raw.buffer.active.length;
while ((!links || links.length === 0) && i >= this.raw.buffer.active.viewportY) {
links = await linkManager.getLinksForType(i, type);
i--;
}

if (!links || links.length < 1) {
return undefined;
}
const event = new TerminalLinkQuickPickEvent(EventType.CLICK);
links[0].activate(event, links[0].text);
return links[0];
}

attachToElement(container: HTMLElement) {
// Update the theme when attaching as the terminal location could have changed
this._updateTheme();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { ILink, Terminal } from 'xterm';
import { XtermTerminal } from 'vs/workbench/contrib/terminal/browser/xterm/xtermTerminal';
import { TerminalConfigHelper } from 'vs/workbench/contrib/terminal/browser/terminalConfigHelper';
import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock';
import { ITerminalConfigHelper, ITerminalConfiguration, ITerminalProcessManager } from 'vs/workbench/contrib/terminal/common/terminal';
import { deepStrictEqual, strictEqual } from 'assert';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService';
import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { IViewDescriptorService } from 'vs/workbench/common/views';
import { IEditorOptions } from 'vs/editor/common/config/editorOptions';
import { ILogService, NullLogService } from 'vs/platform/log/common/log';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { TestStorageService } from 'vs/workbench/test/common/workbenchTestServices';
import { TerminalLocation } from 'vs/platform/terminal/common/terminal';
import { TestViewDescriptorService } from 'vs/workbench/contrib/terminal/test/browser/xterm/xtermTerminal.test';
import { IDetectedLinks, TerminalLinkManager } from 'vs/workbench/contrib/terminal/browser/links/terminalLinkManager';
import { equals } from 'vs/base/common/arrays';
import { ITerminalCapabilityStore } from 'vs/workbench/contrib/terminal/common/capabilities/capabilities';
import { TerminalCapabilityStore } from 'vs/workbench/contrib/terminal/common/capabilities/terminalCapabilityStore';

const defaultTerminalConfig: Partial<ITerminalConfiguration> = {
fontFamily: 'monospace',
fontWeight: 'normal',
fontWeightBold: 'normal',
gpuAcceleration: 'off',
scrollback: 1000,
fastScrollSensitivity: 2,
mouseWheelScrollSensitivity: 1,
unicodeVersion: '11'
};

class TestLinkManager extends TerminalLinkManager {
private _links: IDetectedLinks | undefined;
override async getLinksForType(y: number, type: 'word' | 'web' | 'file'): Promise<ILink[] | undefined> {
switch (type) {
case 'word':
return this._links?.wordLinks?.[y] ? [this._links?.wordLinks?.[y]] : undefined;
case 'web':
return this._links?.webLinks?.[y] ? [this._links?.webLinks?.[y]] : undefined;
case 'file':
return this._links?.fileLinks?.[y] ? [this._links?.fileLinks?.[y]] : undefined;
}
}
setLinks(links: IDetectedLinks): void {
this._links = links;
}
}

suite('TerminalLinkManager', () => {
let instantiationService: TestInstantiationService;
let configurationService: TestConfigurationService;
let themeService: TestThemeService;
let viewDescriptorService: TestViewDescriptorService;
let xterm: XtermTerminal;
let configHelper: ITerminalConfigHelper;
let linkManager: TestLinkManager;

setup(() => {
configurationService = new TestConfigurationService({
editor: {
fastScrollSensitivity: 2,
mouseWheelScrollSensitivity: 1
} as Partial<IEditorOptions>,
terminal: {
integrated: defaultTerminalConfig
}
});
themeService = new TestThemeService();
viewDescriptorService = new TestViewDescriptorService();

instantiationService = new TestInstantiationService();
instantiationService.stub(IConfigurationService, configurationService);
instantiationService.stub(ILogService, new NullLogService());
instantiationService.stub(IStorageService, new TestStorageService());
instantiationService.stub(IThemeService, themeService);
instantiationService.stub(IViewDescriptorService, viewDescriptorService);

configHelper = instantiationService.createInstance(TerminalConfigHelper);
xterm = instantiationService.createInstance(XtermTerminal, Terminal, configHelper, 80, 30, TerminalLocation.Panel, new TerminalCapabilityStore());
linkManager = instantiationService.createInstance(TestLinkManager, xterm, upcastPartial<ITerminalProcessManager>({}), upcastPartial<ITerminalCapabilityStore>({}));
});

suite('getLinks and open recent link', () => {
test('should return no links', async () => {
const links = await linkManager.getLinks();
equals(links.webLinks, []);
equals(links.wordLinks, []);
equals(links.fileLinks, []);
const webLink = await linkManager.openRecentLink('web');
strictEqual(webLink, undefined);
const fileLink = await linkManager.openRecentLink('file');
strictEqual(fileLink, undefined);
});
test('should return word links in order', async () => {
const link1 = {
range: {
start: { x: 1, y: 1 }, end: { x: 14, y: 1 }
},
text: '1_我是学生.txt',
activate: () => Promise.resolve('')
};
const link2 = {
range: {
start: { x: 1, y: 1 }, end: { x: 14, y: 1 }
},
text: '2_我是学生.txt',
activate: () => Promise.resolve('')
};
linkManager.setLinks({ wordLinks: [link1, link2] });
const links = await linkManager.getLinks();
deepStrictEqual(links.wordLinks?.[0].text, link2.text);
deepStrictEqual(links.wordLinks?.[1].text, link1.text);
const webLink = await linkManager.openRecentLink('web');
strictEqual(webLink, undefined);
const fileLink = await linkManager.openRecentLink('file');
strictEqual(fileLink, undefined);
});
test('should return web links in order', async () => {
const link1 = {
range: { start: { x: 5, y: 1 }, end: { x: 40, y: 1 } },
text: 'https://foo.bar/[this is foo site 1]',
activate: () => Promise.resolve('')
};
const link2 = {
range: { start: { x: 5, y: 2 }, end: { x: 40, y: 2 } },
text: 'https://foo.bar/[this is foo site 2]',
activate: () => Promise.resolve('')
};
linkManager.setLinks({ webLinks: [link1, link2] });
const links = await linkManager.getLinks();
deepStrictEqual(links.webLinks?.[0].text, link2.text);
deepStrictEqual(links.webLinks?.[1].text, link1.text);
const webLink = await linkManager.openRecentLink('web');
strictEqual(webLink, link2);
const fileLink = await linkManager.openRecentLink('file');
strictEqual(fileLink, undefined);
});
test('should return file links in order', async () => {
const link1 = {
range: { start: { x: 1, y: 1 }, end: { x: 32, y: 1 } },
text: 'file:///C:/users/test/file_1.txt',
activate: () => Promise.resolve('')
};
const link2 = {
range: { start: { x: 1, y: 2 }, end: { x: 32, y: 2 } },
text: 'file:///C:/users/test/file_2.txt',
activate: () => Promise.resolve('')
};
linkManager.setLinks({ fileLinks: [link1, link2] });
const links = await linkManager.getLinks();
deepStrictEqual(links.fileLinks?.[0].text, link2.text);
deepStrictEqual(links.fileLinks?.[1].text, link1.text);
const webLink = await linkManager.openRecentLink('web');
strictEqual(webLink, undefined);
linkManager.setLinks({ fileLinks: [link2] });
const fileLink = await linkManager.openRecentLink('file');
strictEqual(fileLink, link2);
});
});
});
function upcastPartial<T>(v: Partial<T>): T {
return v as T;
}

0 comments on commit f695611

Please sign in to comment.