Build App / compute-version (push) Successful in 7s
Secret Scan / scan (push) Successful in 8s
Build App / build-macos (push) Successful in 2m53s
Build App / build-linux (push) Successful in 5m12s
Build App / build-windows (push) Successful in 5m15s
Build App / create-tag (push) Successful in 3s
Build App / sync-to-github (push) Successful in 1m5s
Clicking a file path in Claude's terminal output now opens the file in its own window with a CodeMirror 6 editor. The editor highlights the target line, live-reloads while the file changes, and saves explicitly with hash-based conflict detection. The viewer commands are gated by window label. Every app command is now ACL-gated per window through a Tauri AppManifest. build.rs checks the handler list against the capability files and fails the build on any mismatch. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
53 lines
2.1 KiB
TypeScript
53 lines
2.1 KiB
TypeScript
/**
|
|
* xterm `ILinkProvider` for file paths in the buffer.
|
|
*
|
|
* Registered after `WebLinksAddon` so URLs are claimed first; `findFilePathLinks`
|
|
* also refuses anything inside a `scheme://` span, so the two never overlap.
|
|
* Ranges are 1-based on both axes with an *inclusive* end column (xterm's
|
|
* contract), and `provideLinks`' row is 1-based while `getLine` is 0-based.
|
|
*/
|
|
import type { ILink, ILinkProvider, Terminal } from "@xterm/xterm";
|
|
import { findFilePathLinks, type FilePathMatch } from "../../lib/filePathLinks";
|
|
import { joinWrappedRows, offsetToCell } from "../../lib/xtermLineJoin";
|
|
|
|
export interface FilePathHover {
|
|
/** `path` is the raw matched path — relative paths stay relative. */
|
|
show(path: string): void;
|
|
hide(): void;
|
|
}
|
|
|
|
export function createFilePathLinkProvider(
|
|
term: Pick<Terminal, "buffer">,
|
|
onOpen: (match: FilePathMatch) => void,
|
|
gate: (event: MouseEvent) => boolean,
|
|
hover?: FilePathHover,
|
|
): ILinkProvider {
|
|
return {
|
|
provideLinks(bufferLineNumber, callback) {
|
|
const row = bufferLineNumber - 1;
|
|
const line = term.buffer.active.getLine(row);
|
|
if (!line) return callback(undefined);
|
|
const joined = joinWrappedRows(term.buffer.active, row);
|
|
// `offsetToCell` has no row to map onto when nothing was joined.
|
|
if (joined.rowStarts.length === 0) return callback(undefined);
|
|
const matches = findFilePathLinks(joined.text);
|
|
if (matches.length === 0) return callback(undefined);
|
|
const links: ILink[] = matches
|
|
.map((m): ILink => ({
|
|
range: { start: offsetToCell(joined, m.start), end: offsetToCell(joined, m.end - 1) },
|
|
text: joined.text.slice(m.start, m.end),
|
|
decorations: { pointerCursor: true, underline: true },
|
|
activate: (event) => {
|
|
if (!gate(event)) return;
|
|
onOpen(m);
|
|
},
|
|
hover: () => hover?.show(m.path),
|
|
leave: () => hover?.hide(),
|
|
}))
|
|
// Only links that touch the row being asked about (xterm asks per row).
|
|
.filter((l) => l.range.start.y <= bufferLineNumber && l.range.end.y >= bufferLineNumber);
|
|
callback(links.length ? links : undefined);
|
|
},
|
|
};
|
|
}
|