Files
backnotprop__plannotator/packages/review-editor/hooks/useCodeNavPreview.ts
Michael Ramos 95750ab657 feat: search-based code navigation with peek view (#711)
* feat: search-based code navigation with peek view (#694)

Add IDE-like code navigation to the review UI. Cmd/Ctrl+click a token
in a diff to find its definitions and references across the repo via
ripgrep, displayed in a VS Code-style peek panel below the diff.

Backend: bounded rg search with language-aware definition patterns
(TS/JS, Python, Go, Rust), ranked results (same file > changed files >
same directory), confidence labels, and graceful degradation when rg
is not installed. Both Bun and Pi servers implement the endpoints.

Frontend: Dockview peek panel with syntax-highlighted full-file preview
on the left and grouped reference list on the right. Clicking a
reference scrolls the preview; double-clicking an in-diff result
navigates to the file with a gold line flash.

Closes #694

* fix: resolve Pi server TypeScript errors for code-nav

Add missing spawn import, type the close callback parameter,
and use double-cast for parseBody → CodeNavRequest.

* fix: clear loading state on cached preview hits

Without this, clicking a cached file while a fetch is in-flight
leaves isLoading stuck true — the spinner hides the preview.

* feat: show pointer cursor on Cmd/Ctrl+hover for navigable tokens

Adds pn-token-nav class with thicker underline and pointer cursor
when hovering a token while holding the modifier key, signaling
the token is Cmd+clickable for code navigation.

* refactor: remove go-to-diff navigation from peek panel

Strip the in-diff badge, double-click-to-jump, highlightDiffLine
wiring, and onCodeNavGoToDiff from the peek panel. The peek view
is the primary interaction — jump-to-diff adds complexity without
clear value at this stage.

* chore: remove dead code from code-nav cleanup

Delete unused highlightDiffLine.ts, remove codeNavChangedFiles
and codeNavActiveSide from context and App.tsx, drop stale
extractChangedFiles import.

* chore: add code-nav endpoints to AGENTS.md, remove dead activeSide state

* fix: don't classify bare indented calls as definitions

Change the TS/JS method pattern from zero-or-more (*) to
one-or-more (+) declaration keywords, so plain calls like
startServer(config) are no longer misclassified as definitions.

* feat: show toast when code-nav is unavailable in platform-only PR mode

Instead of opening the peek panel and showing misleading "No results",
Cmd+click in non-local PR mode shows a brief toast explaining that
code navigation requires a local checkout.

* fix: strip hljs hardcoded background from code-nav preview

The highlight.js github-dark theme sets a fixed dark background
on all .hljs elements. Apply transparent override for the entire
peek preview so code inherits the active theme's background.
2026-05-13 12:24:37 -07:00

83 lines
2.2 KiB
TypeScript

import { useState, useCallback, useRef } from 'react';
const MAX_CACHE_ENTRIES = 10;
export interface PreviewData {
lines: string[];
startLine: number;
targetLine: number;
filePath: string;
}
export function useCodeNavPreview() {
const [previewData, setPreviewData] = useState<PreviewData | null>(null);
const [isLoading, setIsLoading] = useState(false);
const cacheRef = useRef(new Map<string, string>());
const abortRef = useRef<AbortController | null>(null);
const selectLocation = useCallback(
async (filePath: string, line: number) => {
abortRef.current?.abort();
const controller = new AbortController();
abortRef.current = controller;
const cache = cacheRef.current;
const cached = cache.get(filePath);
if (cached) {
setIsLoading(false);
const allLines = cached.split('\n');
setPreviewData({
lines: allLines,
startLine: 1,
targetLine: line,
filePath,
});
return;
}
setIsLoading(true);
try {
const res = await fetch(
`/api/code-nav/file?path=${encodeURIComponent(filePath)}`,
{ signal: controller.signal },
);
if (!res.ok) throw new Error('Failed');
const data: { content: string } = await res.json();
if (cache.size >= MAX_CACHE_ENTRIES) {
const firstKey = cache.keys().next().value;
if (firstKey) cache.delete(firstKey);
}
cache.set(filePath, data.content);
if (controller.signal.aborted) return;
const allLines = data.content.split('\n');
setPreviewData({
lines: allLines,
startLine: 1,
targetLine: line,
filePath,
});
} catch (err) {
if (err instanceof DOMException && err.name === 'AbortError') return;
setPreviewData(null);
} finally {
if (abortRef.current === controller) {
setIsLoading(false);
}
}
},
[],
);
const clear = useCallback(() => {
abortRef.current?.abort();
setPreviewData(null);
setIsLoading(false);
}, []);
return { previewData, isLoading, selectLocation, clear };
}