Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: fix link range in the double byte character scenario #4261

Closed
wants to merge 2 commits into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
117 changes: 79 additions & 38 deletions addons/xterm-addon-web-links/src/WebLinkProvider.ts
Expand Up @@ -3,11 +3,13 @@
* @license MIT
*/

import { ILinkProvider, ILink, Terminal, IViewportRange } from 'xterm';
import { ILinkProvider, ILink, Terminal, IViewportRange, IBufferLine } from 'xterm';

export interface ILinkProviderOptions {
hover?(event: MouseEvent, text: string, location: IViewportRange): void;

leave?(event: MouseEvent, text: string): void;

urlRegex?: RegExp;
}

Expand Down Expand Up @@ -41,23 +43,33 @@ export class WebLinkProvider implements ILinkProvider {
}
}

export interface IMatchRange {
start: number;
end: number;
text: string;
startX: number;
startY: number;
endX: number;
endY: number;
}

export class LinkComputer {
public static computeLink(y: number, regex: RegExp, terminal: Terminal, activate: (event: MouseEvent, uri: string) => void): ILink[] {
const rex = new RegExp(regex.source, (regex.flags || '') + 'g');

const [line, startLineIndex] = LinkComputer._translateBufferLineToStringWithWrap(y - 1, false, terminal);
const [lines, lineText, startLineIndex] = LinkComputer._translateBufferLineToStringWithWrap(y - 1, false, terminal);

// Don't try if the wrapped line if excessively large as the regex matching will block the main
// thread.
if (line.length > 1024) {
if (lineText.length > 1024) {
return [];
}

let match;
let stringIndex = -1;
const result: ILink[] = [];

while ((match = rex.exec(line)) !== null) {
const matchRanges: IMatchRange[] = [];
while ((match = rex.exec(lineText)) !== null) {
const text = match[1];
if (!text) {
// something matched but does not comply with the given matchIndex
Expand All @@ -70,55 +82,83 @@ export class LinkComputer {
// therefore we cannot use match.index directly, instead we search the position
// of the match group in text again
// also correct regex and string search offsets for the next loop run
stringIndex = line.indexOf(text, stringIndex + 1);
rex.lastIndex = stringIndex + text.length;
if (stringIndex < 0) {
// invalid stringIndex (should not have happened)
break;
let start = -1;
start = lineText.indexOf(text, stringIndex + 1);
let end = -1;
if (start > -1) {
end = start + text.length;
}

let endX = stringIndex + text.length;
let endY = startLineIndex + 1;

while (endX > terminal.cols) {
endX -= terminal.cols;
endY++;
stringIndex = end;
if (start > -1 && end > -1) {
matchRanges.push({
start: start,
text: text,
end: start + text.length,
startX: -1,
startY: -1,
endX: -1,
endY: -1
});
rex.lastIndex = stringIndex + text.length;
}
}

let startX = stringIndex + 1;
let startY = startLineIndex + 1;
while (startX > terminal.cols) {
startX -= terminal.cols;
startY++;
// Convert the matched text position range to buffer offsets range in the double byte character scenario
let stringX = 0;
let lineY = startLineIndex + 1;
let matchIndex = 0;
lines.forEach(line => {
if (line.length > 1024) {
return [];
}
for (let x = 0; x < line.length; x++) {
if (matchRanges[matchIndex]) {
if (stringX === matchRanges[matchIndex].start) {
matchRanges[matchIndex].startX = x + 1;
matchRanges[matchIndex].startY = lineY;
}
if (stringX === matchRanges[matchIndex].end) {
matchRanges[matchIndex].endX = x;
matchRanges[matchIndex].endY = lineY;

matchIndex++;
}
if (line.getCell(x)?.getChars()) {
stringX++;
}
}
}
lineY++;
});

const range = {
start: {
x: startX,
y: startY
return matchRanges.map(r => {
return {
range: {
start: {
x: r.startX,
y: r.startY
},
end: {
x: r.endX,
y: r.endY
}
},
end: {
x: endX,
y: endY
}
text: r.text,
activate
};

result.push({ range, text, activate });
}

return result;
});
}

/**
* Gets the entire line for the buffer line
* @param lineIndex The index of the line being translated.
* @param trimRight Whether to trim whitespace to the right.
*/
private static _translateBufferLineToStringWithWrap(lineIndex: number, trimRight: boolean, terminal: Terminal): [string, number] {
private static _translateBufferLineToStringWithWrap(lineIndex: number, trimRight: boolean, terminal: Terminal): [IBufferLine[], string, number] {
let lineString = '';
let lineWrapsToNext: boolean;
let prevLinesToWrap: boolean;

const lines: IBufferLine[] = [];
do {
const line = terminal.buffer.active.getLine(lineIndex);
if (!line) {
Expand All @@ -141,10 +181,11 @@ export class LinkComputer {
if (!line) {
break;
}
lines.push(line);
lineString += line.translateToString(!lineWrapsToNext && trimRight).substring(0, terminal.cols);
lineIndex++;
} while (lineWrapsToNext);

return [lineString, startLineIndex];
return [lines, lineString, startLineIndex];
}
}