Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -485,7 +485,6 @@ export function useFileTree({
async (absolutePath: string): Promise<void> => {
if (!rootPath || !absolutePath.startsWith(rootPath)) return;

// Collect ancestor directories from rootPath down to the parent of the target
const ancestors: string[] = [];
let current = getParentPath(absolutePath);
while (current.length >= rootPath.length && current !== absolutePath) {
Expand All @@ -494,10 +493,14 @@ export function useFileTree({
current = getParentPath(current);
}

// Expand all ancestors and load their contents
for (const dir of ancestors) {
await expand(dir);
}

const entry = stateRef.current.entriesByPath.get(absolutePath);
if (entry?.kind === "directory") {
await expand(absolutePath);
}
},
[expand, rootPath],
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ export class LinkDetectorAdapter implements ILinkProvider {
event: MouseEvent,
link: DetectedLink,
) => void,
private readonly _onHover?: (event: MouseEvent, link: DetectedLink) => void,
private readonly _onLeave?: () => void,
) {}

provideLinks(
Expand Down Expand Up @@ -192,6 +194,12 @@ export class LinkDetectorAdapter implements ILinkProvider {
activate: (event: MouseEvent) => {
this._onActivate?.(event, detected);
},
hover: (event: MouseEvent) => {
this._onHover?.(event, detected);
},
leave: () => {
this._onLeave?.();
},
});
}
}
Expand Down Expand Up @@ -233,6 +241,12 @@ export class LinkDetectorAdapter implements ILinkProvider {
activate: (event: MouseEvent) => {
this._onActivate?.(event, detected);
},
hover: (event: MouseEvent) => {
this._onHover?.(event, detected);
},
leave: () => {
this._onLeave?.();
},
});
}

Expand Down
11 changes: 11 additions & 0 deletions apps/desktop/src/renderer/lib/terminal/links/word-link-detector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ export class WordLinkDetector implements ILinkProvider {
event: MouseEvent,
resolvedPath: string,
) => void,
private readonly _onHover?: (
event: MouseEvent,
resolvedPath: string,
) => void,
private readonly _onLeave?: () => void,
) {
this._separatorRegex = buildSeparatorRegex(DEFAULT_WORD_SEPARATORS);
}
Expand Down Expand Up @@ -108,6 +113,12 @@ export class WordLinkDetector implements ILinkProvider {
activate: (event: MouseEvent) => {
this._onActivate?.(event, resolved.path);
},
hover: (event: MouseEvent) => {
this._onHover?.(event, resolved.path);
},
leave: () => {
this._onLeave?.();
},
});
}

Expand Down
38 changes: 34 additions & 4 deletions apps/desktop/src/renderer/lib/terminal/terminal-link-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,22 @@ import {
WordLinkDetector,
} from "./links";

export type LinkHoverInfo =
| { kind: "file"; isDirectory: boolean }
| { kind: "url" };

/**
* Link handler callbacks for the v2 terminal.
*/
export interface TerminalLinkHandlers {
/** Called when a file path link is activated (Cmd/Ctrl+click). */
onFileLinkClick?: (event: MouseEvent, link: DetectedLink) => void;
/** Called when a URL link is activated. */
onUrlClick?: (url: string) => void;
onUrlClick?: (event: MouseEvent, url: string) => void;
/** Called when the mouse enters a detected link (file path or URL). */
onLinkHover?: (event: MouseEvent, info: LinkHoverInfo) => void;
/** Called when the mouse leaves a previously hovered link. */
onLinkLeave?: () => void;
/**
* Stat callback to validate file paths exist. Called via the host service
* which handles all path resolution (relative, tilde, etc.) server-side.
Expand Down Expand Up @@ -93,21 +101,39 @@ export class TerminalLinkManager {
this._resolver = new TerminalLinkResolver(handlers.stat);
}

const onLinkHover = handlers.onLinkHover;
const onLinkLeave = handlers.onLinkLeave;

// 1. File path detector (highest priority)
const detector = new LocalLinkDetector(this._resolver);
const adapter = new LinkDetectorAdapter(
this._terminal,
detector,
handlers.onFileLinkClick,
onLinkHover
? (event, link) =>
onLinkHover(event, {
kind: "file",
isDirectory: link.isDirectory,
})
: undefined,
onLinkLeave,
);
this._disposables.push(this._terminal.registerLinkProvider(adapter));

// 2. URL link provider (handles hard-wrapped URLs)
if (handlers.onUrlClick) {
const onUrlClick = handlers.onUrlClick;
const urlProvider = new UrlLinkProvider(this._terminal, (_event, uri) => {
onUrlClick(uri);
});
const urlProvider = new UrlLinkProvider(
this._terminal,
(event, uri) => {
onUrlClick(event, uri);
},
onLinkHover
? (event) => onLinkHover(event, { kind: "url" })
: undefined,
onLinkLeave,
);
this._disposables.push(this._terminal.registerLinkProvider(urlProvider));
}

Expand Down Expand Up @@ -135,6 +161,10 @@ export class TerminalLinkManager {
colEnd: undefined,
});
},
onLinkHover
? (event) => onLinkHover(event, { kind: "file", isDirectory: false })
: undefined,
onLinkLeave,
);
this._disposables.push(this._terminal.registerLinkProvider(wordDetector));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { ProgressAddon } from "@xterm/addon-progress";
import type { SearchAddon } from "@xterm/addon-search";
import type { TerminalAppearance } from "./appearance";
import {
type LinkHoverInfo,
type TerminalLinkHandlers,
TerminalLinkManager,
} from "./terminal-link-manager";
Expand Down Expand Up @@ -224,4 +225,4 @@ if (import.meta.hot) {
import.meta.hot.data.registry = terminalRuntimeRegistry;
}

export type { ConnectionState, TerminalLinkHandlers };
export type { ConnectionState, LinkHoverInfo, TerminalLinkHandlers };
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export {
type OpenInExternalEditorOptions,
useOpenInExternalEditor,
} from "./useOpenInExternalEditor";
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { toast } from "@superset/ui/sonner";
import { eq } from "@tanstack/db";
import { useLiveQuery } from "@tanstack/react-db";
import { useCallback } from "react";
import { electronTrpcClient } from "renderer/lib/trpc-client";
import { useCollections } from "renderer/routes/_authenticated/providers/CollectionsProvider";
import { useLocalHostService } from "renderer/routes/_authenticated/providers/LocalHostServiceProvider";

export interface OpenInExternalEditorOptions {
line?: number;
column?: number;
}

export function useOpenInExternalEditor(workspaceId: string) {
const collections = useCollections();
const { machineId } = useLocalHostService();
const { data: workspacesWithHost = [] } = useLiveQuery(
(q) =>
q
.from({ workspaces: collections.v2Workspaces })
.leftJoin({ hosts: collections.v2Hosts }, ({ workspaces, hosts }) =>
eq(workspaces.hostId, hosts.id),
)
.where(({ workspaces }) => eq(workspaces.id, workspaceId))
.select(({ hosts }) => ({
hostMachineId: hosts?.machineId ?? null,
})),
[collections, workspaceId],
);
const workspaceHost = workspacesWithHost[0];

return useCallback(
(path: string, opts?: OpenInExternalEditorOptions) => {
// Treat unloaded host data as non-local to avoid firing the mutation
// against a potentially remote workspace before locality is confirmed.
if (workspaceHost?.hostMachineId !== machineId) {
toast.error("Can't open remote workspace paths in an external editor");
return;
}
electronTrpcClient.external.openFileInEditor
.mutate({ path, line: opts?.line, column: opts?.column })
.catch((error) => {
console.error("Failed to open in external editor:", error);
toast.error("Failed to open in external editor");
});
},
[workspaceHost, machineId],
);
}
Loading
Loading