xterm-link-provider
Version:
Create a link provider for xterm.js using regex
111 lines • 4.45 kB
JavaScript
export class LinkProvider {
/**
* 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(_terminal, _regex, _handler, _options = {}, _matchIndex = 1) {
this._terminal = _terminal;
this._regex = _regex;
this._handler = _handler;
this._options = _options;
this._matchIndex = _matchIndex;
}
provideLinks(y, callback) {
const links = computeLink(y, this._regex, this._terminal, this._matchIndex).map((_link) => (Object.assign({ 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, regex, 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 = [];
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, terminal) => {
let lineString = '';
let lineWrapsToNext;
let prevLinesToWrap;
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, lineIndex, stringIndex, reportLastCell = false) => {
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 };
};
//# sourceMappingURL=index.js.map