xterm-link-provider
Version:
Create a link provider for xterm.js using regex
154 lines (135 loc) • 4.61 kB
text/typescript
import type {IBufferCellPosition, ILink, ILinkProvider, Terminal} from '@xterm/xterm';
type ILinkProviderOptions = Omit<ILink, 'range' | 'text' | 'activate'>;
export class LinkProvider implements ILinkProvider {
/**
* Create a Link Provider for xterm.js
* @param _terminal The terminal instance
* @param _regex The regular expression to use for matching
* @param _handler Callback for when link is clicked
* @param _options Further hooks, eg. hover, leave and decorations
* @param _matchIndex The index to use from regexp.exec result, default 1
*/
constructor(
private readonly _terminal: Terminal,
private readonly _regex: RegExp,
private readonly _handler: ILink['activate'],
private readonly _options: ILinkProviderOptions = {},
private readonly _matchIndex = 1
) {}
public provideLinks(y: number, callback: (links: ILink[] | undefined) => void): void {
const links = computeLink(y, this._regex, this._terminal, this._matchIndex).map(
(_link): ILink => ({
range: _link.range,
text: _link.text,
activate: this._handler,
...this._options
})
);
callback(links);
}
}
/**
* Find link range and text for the given line and regex
* @param y The line number to process
* @param regex The regular expression to use for matching
* @param terminal The terminal instance
* @param matchIndex The index to use from regexp.exec result, default 1
*/
export const computeLink = (y: number, regex: RegExp, terminal: Terminal, matchIndex = 1) => {
const rex = new RegExp(
regex.source,
((regex.flags || '') + 'g')
.split('')
.filter((value, index, arr) => arr.indexOf(value) === index)
.join('')
);
const [line, startLineIndex] = translateBufferLineToStringWithWrap(y - 1, terminal);
let match;
let stringIndex = -1;
const result: Pick<ILink, 'range' | 'text'>[] = [];
while ((match = rex.exec(line)) !== null) {
const text = match[matchIndex];
if (!text) {
// something matched but does not comply with the given matchIndex
// since this is most likely a bug the regex itself we simply do nothing here
console.log('match found without corresponding matchIndex');
break;
}
// Get index, match.index is for the outer match which includes negated chars
// 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;
}
const range = {
start: stringIndexToBufferPosition(terminal, startLineIndex, stringIndex),
end: stringIndexToBufferPosition(
terminal,
startLineIndex,
stringIndex + text.length - 1,
true
)
};
result.push({range, text});
}
return result;
};
const translateBufferLineToStringWithWrap = (
lineIndex: number,
terminal: Terminal
): [string, number] => {
let lineString = '';
let lineWrapsToNext: boolean;
let prevLinesToWrap: boolean;
do {
const line = terminal.buffer.active.getLine(lineIndex);
if (!line) {
break;
}
if (line.isWrapped) {
lineIndex--;
}
prevLinesToWrap = line.isWrapped;
} while (prevLinesToWrap);
const startLineIndex = lineIndex;
do {
const nextLine = terminal.buffer.active.getLine(lineIndex + 1);
lineWrapsToNext = nextLine ? nextLine.isWrapped : false;
const line = terminal.buffer.active.getLine(lineIndex);
if (!line) {
break;
}
lineString += line.translateToString(true).substring(0, terminal.cols);
lineIndex++;
} while (lineWrapsToNext);
return [lineString, startLineIndex];
};
const stringIndexToBufferPosition = (
terminal: Terminal,
lineIndex: number,
stringIndex: number,
reportLastCell = false
): IBufferCellPosition => {
const cell = terminal.buffer.active.getNullCell();
while (stringIndex) {
const line = terminal.buffer.active.getLine(lineIndex);
if (!line) {
return {x: 0, y: 0};
}
const length = line.length;
for (let i = 0; i < length; ) {
line.getCell(i, cell);
stringIndex -= cell.getChars().length;
if (stringIndex < 0) {
return {x: i + (reportLastCell ? cell.getWidth() : 1), y: lineIndex + 1};
}
i += cell.getWidth();
}
lineIndex++;
}
return {x: 1, y: lineIndex + 1};
};