未验证 提交 304fc630 编写于 作者: D Daniel Imms 提交者: GitHub

Merge pull request #90336 from jmbockhorst/linkProvider

Adopt terminal link provider API
......@@ -5,7 +5,7 @@
import * as nls from 'vs/nls';
import { URI } from 'vs/base/common/uri';
import { DisposableStore } from 'vs/base/common/lifecycle';
import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle';
import { IOpenerService } from 'vs/platform/opener/common/opener';
import { TerminalWidgetManager, WidgetVerticalAlignment } from 'vs/workbench/contrib/terminal/browser/terminalWidgetManager';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
......@@ -13,7 +13,7 @@ import { ITerminalProcessManager, ITerminalConfigHelper } from 'vs/workbench/con
import { ITextEditorSelection } from 'vs/platform/editor/common/editor';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { IFileService } from 'vs/platform/files/common/files';
import { Terminal, ILinkMatcherOptions, IViewportRange } from 'xterm';
import { Terminal, ILinkMatcherOptions, IViewportRange, ITerminalAddon } from 'xterm';
import { REMOTE_HOST_SCHEME } from 'vs/platform/remote/common/remoteHosts';
import { posix, win32 } from 'vs/base/common/path';
import { ITerminalInstanceService, ITerminalBeforeHandleLinkEvent, LINK_INTERCEPT_THRESHOLD } from 'vs/workbench/contrib/terminal/browser/terminal';
......@@ -21,6 +21,7 @@ import { OperatingSystem, isMacintosh } from 'vs/base/common/platform';
import { IMarkdownString, MarkdownString } from 'vs/base/common/htmlContent';
import { Emitter, Event } from 'vs/base/common/event';
import { ILogService } from 'vs/platform/log/common/log';
import { TerminalWebLinkProvider } from 'vs/workbench/contrib/terminal/browser/terminalWebLinkProvider';
const pathPrefix = '(\\.\\.?|\\~)';
const pathSeparatorClause = '\\/';
......@@ -75,6 +76,9 @@ export class TerminalLinkHandler extends DisposableStore {
private _gitDiffPostImagePattern: RegExp;
private readonly _tooltipCallback: (event: MouseEvent, uri: string, location: IViewportRange, linkHandler: (url: string) => void) => boolean | void;
private readonly _leaveCallback: () => void;
private _linkMatchers: number[] = [];
private _webLinksAddon: ITerminalAddon | undefined;
private _linkProvider: IDisposable | undefined;
private _hasBeforeHandleLinkListeners = false;
protected static _LINK_INTERCEPT_THRESHOLD = LINK_INTERCEPT_THRESHOLD;
......@@ -153,6 +157,26 @@ export class TerminalLinkHandler extends DisposableStore {
}
};
if (this._configHelper.config.experimentalLinkProvider) {
this.registerLinkProvider();
} else {
this._registerLinkMatchers();
}
this._configurationService?.onDidChangeConfiguration(e => {
if (e.affectsConfiguration('terminal.integrated.experimentalLinkProvider')) {
if (this._configHelper.config.experimentalLinkProvider) {
this._deregisterLinkMatchers();
this.registerLinkProvider();
} else {
this._linkProvider?.dispose();
this._registerLinkMatchers();
}
}
});
}
private _registerLinkMatchers() {
this.registerWebLinkHandler();
if (this._processManager) {
if (this._configHelper.config.enableFileLinks) {
......@@ -162,6 +186,14 @@ export class TerminalLinkHandler extends DisposableStore {
}
}
private _deregisterLinkMatchers() {
this._webLinksAddon?.dispose();
this._linkMatchers.forEach(matcherId => {
this._xterm.deregisterLinkMatcher(matcherId);
});
}
public setWidgetManager(widgetManager: TerminalWidgetManager): void {
this._widgetManager = widgetManager;
}
......@@ -192,18 +224,19 @@ export class TerminalLinkHandler extends DisposableStore {
if (!this._xterm) {
return;
}
const wrappedHandler = this._wrapLinkHandler(uri => {
this._handleHypertextLink(uri);
const wrappedHandler = this._wrapLinkHandler(link => {
this._handleHypertextLink(link);
});
const tooltipCallback = (event: MouseEvent, uri: string, location: IViewportRange) => {
this._tooltipCallback(event, uri, location, this._handleHypertextLink.bind(this));
};
this._xterm.loadAddon(new WebLinksAddon(wrappedHandler, {
validationCallback: (uri: string, callback: (isValid: boolean) => void) => this._validateWebLink(uri, callback),
this._webLinksAddon = new WebLinksAddon(wrappedHandler, {
validationCallback: (uri: string, callback: (isValid: boolean) => void) => this._validateWebLink(callback),
tooltipCallback,
leaveCallback: this._leaveCallback,
willLinkActivate: (e: MouseEvent) => this._isLinkActivationModifierDown(e)
}));
});
this._xterm.loadAddon(this._webLinksAddon);
});
}
......@@ -214,13 +247,13 @@ export class TerminalLinkHandler extends DisposableStore {
const tooltipCallback = (event: MouseEvent, uri: string, location: IViewportRange) => {
this._tooltipCallback(event, uri, location, this._handleLocalLink.bind(this));
};
this._xterm.registerLinkMatcher(this._localLinkRegex, wrappedHandler, {
this._linkMatchers.push(this._xterm.registerLinkMatcher(this._localLinkRegex, wrappedHandler, {
validationCallback: (uri: string, callback: (isValid: boolean) => void) => this._validateLocalLink(uri, callback),
tooltipCallback,
leaveCallback: this._leaveCallback,
willLinkActivate: (e: MouseEvent) => this._isLinkActivationModifierDown(e),
priority: LOCAL_LINK_PRIORITY
});
}));
}
public registerGitDiffLinkHandlers(): void {
......@@ -238,8 +271,17 @@ export class TerminalLinkHandler extends DisposableStore {
willLinkActivate: (e: MouseEvent) => this._isLinkActivationModifierDown(e),
priority: LOCAL_LINK_PRIORITY
};
this._xterm.registerLinkMatcher(this._gitDiffPreImagePattern, wrappedHandler, options);
this._xterm.registerLinkMatcher(this._gitDiffPostImagePattern, wrappedHandler, options);
this._linkMatchers.push(this._xterm.registerLinkMatcher(this._gitDiffPreImagePattern, wrappedHandler, options));
this._linkMatchers.push(this._xterm.registerLinkMatcher(this._gitDiffPostImagePattern, wrappedHandler, options));
}
public registerLinkProvider(): void {
// Web links
const tooltipCallback = (event: MouseEvent, link: string, location: IViewportRange) => {
this._tooltipCallback(event, link, location, this._handleHypertextLink.bind(this, link));
};
const wrappedActivateCallback = this._wrapLinkHandler(this._handleHypertextLink.bind(this));
this._linkProvider = this._xterm.registerLinkProvider(new TerminalWebLinkProvider(this._xterm, wrappedActivateCallback, tooltipCallback, this._leaveCallback));
}
protected _wrapLinkHandler(handler: (link: string) => void): XtermLinkMatcherHandler {
......@@ -313,7 +355,7 @@ export class TerminalLinkHandler extends DisposableStore {
this._resolvePath(link).then(resolvedLink => callback(!!resolvedLink));
}
private _validateWebLink(link: string, callback: (isValid: boolean) => void): void {
private _validateWebLink(callback: (isValid: boolean) => void): void {
callback(true);
}
......
/*---------------------------------------------------------------------------------------------
* 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, ILinkProvider, IBufferCellPosition, ILink, IBufferRange, IBuffer, IBufferLine } from 'xterm';
import { ILinkComputerTarget, LinkComputer } from 'vs/editor/common/modes/linkComputer';
import { IRange } from 'vs/editor/common/core/range';
export class TerminalWebLinkProvider implements ILinkProvider {
private _linkComputerTarget: ILinkComputerTarget | undefined;
constructor(
private readonly _xterm: Terminal,
private readonly _activateCallback: (event: MouseEvent, uri: string) => void,
private readonly _tooltipCallback: (event: MouseEvent, uri: string, location: IViewportRange) => boolean | void,
private readonly _leaveCallback: () => void
) {
}
public provideLink(position: IBufferCellPosition, callback: (link: ILink | undefined) => void): void {
let startLine = position.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++;
}
this._linkComputerTarget = new TerminalLinkAdapter(this._xterm, startLine, endLine);
const links = LinkComputer.computeLinks(this._linkComputerTarget);
let found = false;
links.forEach(link => {
const range = convertLinkRangeToBuffer(lines, this._xterm.cols, link.range, startLine);
// Check if the link if within the mouse position
if (this._positionIsInRange(position, range)) {
found = true;
callback({
text: link.url?.toString() || '',
range,
activate: (event: MouseEvent, text: string) => {
this._activateCallback(event, text);
},
hover: (event: MouseEvent, text: string) => {
setTimeout(() => {
this._tooltipCallback(event, text, convertBufferRangeToViewport(range, this._xterm.buffer.active.viewportY));
}, 200);
},
leave: () => {
this._leaveCallback();
}
});
}
});
if (!found) {
callback(undefined);
}
}
private _positionIsInRange(position: IBufferCellPosition, range: IBufferRange): boolean {
if (position.y < range.start.y || position.y > range.end.y) {
return false;
}
if (position.y === range.start.y && position.x < range.start.x) {
return false;
}
if (position.y === range.end.y && position.x > range.end.x) {
return false;
}
return true;
}
}
export function convertLinkRangeToBuffer(lines: IBufferLine[], bufferWidth: number, range: IRange, startLine: number) {
const bufferRange: IBufferRange = {
start: {
x: range.startColumn,
y: range.startLineNumber + startLine
},
end: {
x: range.endColumn - 1,
y: range.endLineNumber + startLine
}
};
// Shift start range right for each wide character before the link
let startOffset = 0;
const startWrappedLineCount = Math.ceil(range.startColumn / bufferWidth);
for (let y = 0; y < startWrappedLineCount; y++) {
const lineLength = Math.min(bufferWidth, range.startColumn - y * bufferWidth);
let lineOffset = 0;
const line = lines[y];
for (let x = 0; x < Math.min(bufferWidth, lineLength + lineOffset); x++) {
const width = line.getCell(x)?.getWidth();
if (width === 2) {
lineOffset++;
}
}
startOffset += lineOffset;
}
// Shift end range right for each wide character inside the link
let endOffset = 0;
const endWrappedLineCount = Math.ceil(range.endColumn / bufferWidth);
for (let y = startWrappedLineCount - 1; y < endWrappedLineCount; y++) {
const start = (y === startWrappedLineCount - 1 ? (range.startColumn + startOffset) % bufferWidth : 0);
const lineLength = Math.min(bufferWidth, range.endColumn + startOffset - y * bufferWidth);
const startLineOffset = (y === startWrappedLineCount - 1 ? startOffset : 0);
let lineOffset = 0;
const line = lines[y];
for (let x = start; x < Math.min(bufferWidth, lineLength + lineOffset + startLineOffset); x++) {
const cell = line.getCell(x)!;
const width = cell.getWidth();
// Offset for 0 cells following wide characters
if (width === 2) {
lineOffset++;
}
// Offset for early wrapping when the last cell in row is a wide character
if (x === bufferWidth - 1 && cell.getChars() === '') {
lineOffset++;
}
}
endOffset += lineOffset;
}
// Apply the width character offsets to the result
bufferRange.start.x += startOffset;
bufferRange.end.x += startOffset + endOffset;
// Convert back to wrapped lines
while (bufferRange.start.x > bufferWidth) {
bufferRange.start.x -= bufferWidth;
bufferRange.start.y++;
}
while (bufferRange.end.x > bufferWidth) {
bufferRange.end.x -= bufferWidth;
bufferRange.end.y++;
}
return bufferRange;
}
function convertBufferRangeToViewport(bufferRange: IBufferRange, viewportY: number): IViewportRange {
return {
start: {
x: bufferRange.start.x - 1,
y: bufferRange.start.y - viewportY - 1
},
end: {
x: bufferRange.end.x - 1,
y: bufferRange.end.y - viewportY - 1
}
};
}
class TerminalLinkAdapter implements ILinkComputerTarget {
constructor(
private _xterm: Terminal,
private _lineStart: number,
private _lineEnd: number
) { }
getLineCount(): number {
return 1;
}
getLineContent(): string {
return getXtermLineContent(this._xterm.buffer.active, this._lineStart, this._lineEnd);
}
}
function getXtermLineContent(buffer: IBuffer, lineStart: number, lineEnd: number): string {
let line = '';
for (let i = lineStart; i <= lineEnd; i++) {
line += buffer.getLine(i)?.translateToString(true);
}
return line;
}
......@@ -51,7 +51,7 @@ export class TerminalWidgetManager implements IDisposable {
}
public showMessage(left: number, y: number, text: IMarkdownString, verticalAlignment: WidgetVerticalAlignment = WidgetVerticalAlignment.Bottom, linkHandler: (url: string) => void): void {
if (!this._container) {
if (!this._container || this._messageWidget?.mouseOver) {
return;
}
dispose(this._messageWidget);
......@@ -61,8 +61,9 @@ export class TerminalWidgetManager implements IDisposable {
public closeMessage(): void {
this._messageListeners.clear();
const currentWidget = this._messageWidget;
setTimeout(() => {
if (this._messageWidget && !this._messageWidget.mouseOver) {
if (this._messageWidget && !this._messageWidget.mouseOver && this._messageWidget === currentWidget) {
this._messageListeners.add(MessageWidget.fadeOut(this._messageWidget));
}
}, 50);
......
......@@ -131,6 +131,7 @@ export interface ITerminalConfiguration {
experimentalUseTitleEvent: boolean;
enableFileLinks: boolean;
unicodeVersion: '6' | '11';
experimentalLinkProvider: boolean;
}
export interface ITerminalConfigHelper {
......
......@@ -300,6 +300,11 @@ export const terminalConfiguration: IConfigurationNode = {
],
default: '11',
description: localize('terminal.integrated.unicodeVersion', "Controls what version of unicode to use when evaluating the width of characters in the terminal. If you experience emoji or other wide characters not taking up the right amount of space or backspace either deleting too much or too little then you may want to try tweaking this setting.")
},
'terminal.integrated.experimentalLinkProvider': {
description: localize('terminal.integrated.experimentalLinkProvider', "An experimental setting that aims to improve link detection in the terminal by improving when links are detected and by enabling shared link detection with the editor. Currently this only supports web links."),
type: 'boolean',
default: false
}
}
};
......
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { convertLinkRangeToBuffer, TerminalWebLinkProvider } from 'vs/workbench/contrib/terminal/browser/terminalWebLinkProvider';
import { IBufferLine, IBufferCell, Terminal, ILink, IBufferRange, IBufferCellPosition } from 'xterm';
suite('Workbench - TerminalWebLinkProvider', () => {
suite('TerminalWebLinkProvider', () => {
async function assertLink(text: string, expected: { text: string, range: [number, number][] }) {
const xterm = new Terminal();
const provider = new TerminalWebLinkProvider(xterm, () => { }, () => { }, () => { });
// Write the text and wait for the parser to finish
await new Promise<void>(r => xterm.write(text, r));
// Calculate positions just outside of link boundaries
const noLinkPositions: IBufferCellPosition[] = [
{ x: expected.range[0][0] - 1, y: expected.range[0][1] },
{ x: expected.range[1][0] + 1, y: expected.range[1][1] }
];
// Ensure outside positions do not detect the link
for (let i = 0; i < noLinkPositions.length; i++) {
const link = await new Promise<ILink | undefined>(r => provider.provideLink(noLinkPositions[i], r));
assert.equal(link, undefined, `Just outside range boundary should not result in link, link found at: (${link?.range.start.x}, ${link?.range.start.y}) to (${link?.range.end.x}, ${link?.range.end.y})`);
}
// Convert range from [[startx, starty], [endx, endy]] to an IBufferRange
const linkRange: IBufferRange = {
start: { x: expected.range[0][0], y: expected.range[0][1] },
end: { x: expected.range[1][0], y: expected.range[1][1] },
};
// Calculate positions inside the link boundaries
const linkPositions: IBufferCellPosition[] = [
linkRange.start,
linkRange.end
];
// Ensure inside positions do detect the link
for (let i = 0; i < linkPositions.length; i++) {
const link = await new Promise<ILink | undefined>(r => provider.provideLink(linkPositions[i], r));
assert.ok(link);
assert.deepEqual(link!.text, expected.text);
assert.deepEqual(link!.range, linkRange);
}
}
// These tests are based on LinkComputer.test.ts
test('LinkComputer cases', async () => {
await assertLink('x = "http://foo.bar";', { range: [[6, 1], [19, 1]], text: 'http://foo.bar' });
await assertLink('x = (http://foo.bar);', { range: [[6, 1], [19, 1]], text: 'http://foo.bar' });
await assertLink('x = \'http://foo.bar\';', { range: [[6, 1], [19, 1]], text: 'http://foo.bar' });
await assertLink('x = http://foo.bar ;', { range: [[6, 1], [19, 1]], text: 'http://foo.bar' });
await assertLink('x = <http://foo.bar>;', { range: [[6, 1], [19, 1]], text: 'http://foo.bar' });
await assertLink('x = {http://foo.bar};', { range: [[6, 1], [19, 1]], text: 'http://foo.bar' });
await assertLink('(see http://foo.bar)', { range: [[6, 1], [19, 1]], text: 'http://foo.bar' });
await assertLink('[see http://foo.bar]', { range: [[6, 1], [19, 1]], text: 'http://foo.bar' });
await assertLink('{see http://foo.bar}', { range: [[6, 1], [19, 1]], text: 'http://foo.bar' });
await assertLink('<see http://foo.bar>', { range: [[6, 1], [19, 1]], text: 'http://foo.bar' });
await assertLink('<url>http://foo.bar</url>', { range: [[6, 1], [19, 1]], text: 'http://foo.bar' });
await assertLink('// Click here to learn more. https://go.microsoft.com/fwlink/?LinkID=513275&clcid=0x409', { range: [[30, 1], [7, 2]], text: 'https://go.microsoft.com/fwlink/?LinkID=513275&clcid=0x409' });
await assertLink('// Click here to learn more. https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx', { range: [[30, 1], [28, 2]], text: 'https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx' });
await assertLink('// https://github.com/projectkudu/kudu/blob/master/Kudu.Core/Scripts/selectNodeVersion.js', { range: [[4, 1], [9, 2]], text: 'https://github.com/projectkudu/kudu/blob/master/Kudu.Core/Scripts/selectNodeVersion.js' });
await assertLink('<!-- !!! Do not remove !!! WebContentRef(link:https://go.microsoft.com/fwlink/?LinkId=166007, area:Admin, updated:2015, nextUpdate:2016, tags:SqlServer) !!! Do not remove !!! -->', { range: [[49, 1], [14, 2]], text: 'https://go.microsoft.com/fwlink/?LinkId=166007' });
await assertLink('For instructions, see https://go.microsoft.com/fwlink/?LinkId=166007.</value>', { range: [[23, 1], [68, 1]], text: 'https://go.microsoft.com/fwlink/?LinkId=166007' });
await assertLink('For instructions, see https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx.</value>', { range: [[23, 1], [21, 2]], text: 'https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx' });
await assertLink('x = "https://en.wikipedia.org/wiki/Zürich";', { range: [[6, 1], [41, 1]], text: 'https://en.wikipedia.org/wiki/Zürich' });
await assertLink('請參閱 http://go.microsoft.com/fwlink/?LinkId=761051。', { range: [[8, 1], [53, 1]], text: 'http://go.microsoft.com/fwlink/?LinkId=761051' });
await assertLink('(請參閱 http://go.microsoft.com/fwlink/?LinkId=761051)', { range: [[10, 1], [55, 1]], text: 'http://go.microsoft.com/fwlink/?LinkId=761051' });
await assertLink('x = "file:///foo.bar";', { range: [[6, 1], [20, 1]], text: 'file:///foo.bar' });
await assertLink('x = "file://c:/foo.bar";', { range: [[6, 1], [22, 1]], text: 'file://c:/foo.bar' });
await assertLink('x = "file://shares/foo.bar";', { range: [[6, 1], [26, 1]], text: 'file://shares/foo.bar' });
await assertLink('x = "file://shäres/foo.bar";', { range: [[6, 1], [26, 1]], text: 'file://shäres/foo.bar' });
await assertLink('Some text, then http://www.bing.com.', { range: [[17, 1], [35, 1]], text: 'http://www.bing.com' });
await assertLink('let url = `http://***/_api/web/lists/GetByTitle(\'Teambuildingaanvragen\')/items`;', { range: [[12, 1], [78, 1]], text: 'http://***/_api/web/lists/GetByTitle(\'Teambuildingaanvragen\')/items' });
await assertLink('7. At this point, ServiceMain has been called. There is no functionality presently in ServiceMain, but you can consult the [MSDN documentation](https://msdn.microsoft.com/en-us/library/windows/desktop/ms687414(v=vs.85).aspx) to add functionality as desired!', { range: [[66, 2], [64, 3]], text: 'https://msdn.microsoft.com/en-us/library/windows/desktop/ms687414(v=vs.85).aspx' });
await assertLink('let x = "http://[::1]:5000/connect/token"', { range: [[10, 1], [40, 1]], text: 'http://[::1]:5000/connect/token' });
await assertLink('2. Navigate to **https://portal.azure.com**', { range: [[18, 1], [41, 1]], text: 'https://portal.azure.com' });
await assertLink('POST|https://portal.azure.com|2019-12-05|', { range: [[6, 1], [29, 1]], text: 'https://portal.azure.com' });
await assertLink('aa https://foo.bar/[this is foo site] aa', { range: [[5, 1], [38, 1]], text: 'https://foo.bar/[this is foo site]' });
});
});
suite('convertLinkRangeToBuffer', () => {
test('should convert ranges for ascii characters', () => {
const lines = createBufferLineArray([
{ text: 'AA http://t', width: 11 },
{ text: '.com/f/', width: 8 }
]);
const bufferRange = convertLinkRangeToBuffer(lines, 11, { startColumn: 4, startLineNumber: 1, endColumn: 19, endLineNumber: 1 }, 0);
assert.deepEqual(bufferRange, {
start: { x: 4, y: 1 },
end: { x: 7, y: 2 }
});
});
test('should convert ranges for wide characters before the link', () => {
const lines = createBufferLineArray([
{ text: 'A文 http://', width: 11 },
{ text: 't.com/f/', width: 9 }
]);
const bufferRange = convertLinkRangeToBuffer(lines, 11, { startColumn: 4, startLineNumber: 1, endColumn: 19, endLineNumber: 1 }, 0);
assert.deepEqual(bufferRange, {
start: { x: 4 + 1, y: 1 },
end: { x: 7 + 1, y: 2 }
});
});
test('should convert ranges for wide characters inside the link', () => {
const lines = createBufferLineArray([
{ text: 'AA http://t', width: 11 },
{ text: '.com/文/', width: 8 }
]);
const bufferRange = convertLinkRangeToBuffer(lines, 11, { startColumn: 4, startLineNumber: 1, endColumn: 19, endLineNumber: 1 }, 0);
assert.deepEqual(bufferRange, {
start: { x: 4, y: 1 },
end: { x: 7 + 1, y: 2 }
});
});
test('should convert ranges for wide characters before and inside the link', () => {
const lines = createBufferLineArray([
{ text: 'A文 http://', width: 11 },
{ text: 't.com/文/', width: 9 }
]);
const bufferRange = convertLinkRangeToBuffer(lines, 11, { startColumn: 4, startLineNumber: 1, endColumn: 19, endLineNumber: 1 }, 0);
assert.deepEqual(bufferRange, {
start: { x: 4 + 1, y: 1 },
end: { x: 7 + 2, y: 2 }
});
});
test('should convert ranges for ascii characters (link starts on wrapped)', () => {
const lines = createBufferLineArray([
{ text: 'AAAAAAAAAAA', width: 11 },
{ text: 'AA http://t', width: 11 },
{ text: '.com/f/', width: 8 }
]);
const bufferRange = convertLinkRangeToBuffer(lines, 11, { startColumn: 15, startLineNumber: 1, endColumn: 30, endLineNumber: 1 }, 0);
assert.deepEqual(bufferRange, {
start: { x: 4, y: 2 },
end: { x: 7, y: 3 }
});
});
test('should convert ranges for wide characters before the link (link starts on wrapped)', () => {
const lines = createBufferLineArray([
{ text: 'AAAAAAAAAAA', width: 11 },
{ text: 'A文 http://', width: 11 },
{ text: 't.com/f/', width: 9 }
]);
const bufferRange = convertLinkRangeToBuffer(lines, 11, { startColumn: 15, startLineNumber: 1, endColumn: 30, endLineNumber: 1 }, 0);
assert.deepEqual(bufferRange, {
start: { x: 4 + 1, y: 2 },
end: { x: 7 + 1, y: 3 }
});
});
test('should convert ranges for wide characters inside the link (link starts on wrapped)', () => {
const lines = createBufferLineArray([
{ text: 'AAAAAAAAAAA', width: 11 },
{ text: 'AA http://t', width: 11 },
{ text: '.com/文/', width: 8 }
]);
const bufferRange = convertLinkRangeToBuffer(lines, 11, { startColumn: 15, startLineNumber: 1, endColumn: 30, endLineNumber: 1 }, 0);
assert.deepEqual(bufferRange, {
start: { x: 4, y: 2 },
end: { x: 7 + 1, y: 3 }
});
});
test('should convert ranges for wide characters before and inside the link', () => {
const lines = createBufferLineArray([
{ text: 'AAAAAAAAAAA', width: 11 },
{ text: 'A文 http://', width: 11 },
{ text: 't.com/文/', width: 9 }
]);
const bufferRange = convertLinkRangeToBuffer(lines, 11, { startColumn: 15, startLineNumber: 1, endColumn: 30, endLineNumber: 1 }, 0);
assert.deepEqual(bufferRange, {
start: { x: 4 + 1, y: 2 },
end: { x: 7 + 2, y: 3 }
});
});
test('should convert ranges for several wide characters before the link', () => {
const lines = createBufferLineArray([
{ text: 'A文文AAAAAA', width: 11 },
{ text: 'AA文文 http', width: 11 },
{ text: '://t.com/f/', width: 11 }
]);
const bufferRange = convertLinkRangeToBuffer(lines, 11, { startColumn: 15, startLineNumber: 1, endColumn: 30, endLineNumber: 1 }, 0);
// This test ensures that the start offset is applies to the end before it's counted
assert.deepEqual(bufferRange, {
start: { x: 4 + 4, y: 2 },
end: { x: 7 + 4, y: 3 }
});
});
test('should convert ranges for several wide characters before and inside the link', () => {
const lines = createBufferLineArray([
{ text: 'A文文AAAAAA', width: 11 },
{ text: 'AA文文 http', width: 11 },
{ text: '://t.com/文', width: 11 },
{ text: '文/', width: 3 }
]);
const bufferRange = convertLinkRangeToBuffer(lines, 11, { startColumn: 15, startLineNumber: 1, endColumn: 31, endLineNumber: 1 }, 0);
// This test ensures that the start offset is applies to the end before it's counted
assert.deepEqual(bufferRange, {
start: { x: 4 + 4, y: 2 },
end: { x: 2, y: 4 }
});
});
});
});
const TEST_WIDE_CHAR = '';
const TEST_NULL_CHAR = 'C';
function createBufferLineArray(lines: { text: string, width: number }[]): IBufferLine[] {
let result: IBufferLine[] = [];
lines.forEach((l, i) => {
result.push(new TestBufferLine(
l.text,
l.width,
i + 1 !== lines.length
));
});
return result;
}
class TestBufferLine implements IBufferLine {
constructor(
private _text: string,
public length: number,
public isWrapped: boolean
) {
}
getCell(x: number): IBufferCell | undefined {
// Create a fake line of cells and use that to resolve the width
let cells: string = '';
let offset = 0;
for (let i = 0; i <= x - offset; i++) {
const char = this._text.charAt(i);
cells += char;
if (this._text.charAt(i) === TEST_WIDE_CHAR) {
// Skip the next character as it's width is 0
cells += TEST_NULL_CHAR;
offset++;
}
}
return {
getChars: () => {
return x >= cells.length ? '' : cells.charAt(x);
},
getWidth: () => {
switch (cells.charAt(x)) {
case TEST_WIDE_CHAR: return 2;
case TEST_NULL_CHAR: return 0;
default: return 1;
}
}
} as any;
}
translateToString(): string {
throw new Error('Method not implemented.');
}
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册