Files
devdesk/web/src/links.js
T

46 lines
1.8 KiB
JavaScript
Raw Normal View History

// File paths in the terminal become links (devdesk#21): click one in Claude's
// output (web/src/files.js, ./notes.md:12, /root/code/x/y.py:3:9) to open it in
// the Files pane, at that line if there is one. Only paths that are files of
// the session become links, so words and URLs are left alone.
// A path: optional ./ ../ or /, segments, and either a slash or an extension;
// then an optional :line[:col].
const PATH_RE = /(?<![\w/.@~+-])((?:\.{1,2}\/|\/)?(?:[\w@+~-][\w.@+~-]*\/)*[\w@+~-][\w.@+~-]*(?:\.[A-Za-z0-9]{1,10})?)(?::(\d+)(?::(\d+))?)?/g;
const URL_RE = /\b[a-z][a-z0-9+.-]*:\/\/\S+/gi;
export function findPaths(text) {
const urls = [...text.matchAll(URL_RE)].map((m) => [m.index, m.index + m[0].length]);
const out = [];
for (const m of text.matchAll(PATH_RE)) {
const candidate = m[1];
if (!candidate.includes('/') && !/\.[A-Za-z0-9]{1,10}$/.test(candidate)) continue;
if (urls.some(([a, b]) => m.index >= a && m.index < b)) continue;
out.push({ index: m.index, length: m[0].length, path: candidate, line: m[2] ? Number(m[2]) : undefined });
}
return out;
}
export function registerPathLinks(term, { resolve, open }) {
term.registerLinkProvider({
provideLinks(y, callback) {
const line = term.buffer.active.getLine(y - 1);
if (!line) {
callback(undefined);
return;
}
const links = [];
for (const found of findPaths(line.translateToString(true))) {
const path = resolve(found.path);
if (!path) continue;
links.push({
range: { start: { x: found.index + 1, y }, end: { x: found.index + found.length, y } },
text: path,
decorations: { underline: true, pointerCursor: true },
activate: () => open(path, found.line),
});
}
callback(links.length ? links : undefined);
},
});
}