From f7ded19014a9ddde3b7e372f9996a402bd363dc4 Mon Sep 17 00:00:00 2001 From: bkellam Date: Tue, 24 Jun 2025 16:19:14 -0700 Subject: [PATCH 1/7] move file source & tree preview data fetching to server --- .../[...path]/components/codePreviewPanel.tsx | 64 ++++------- .../components/pureTreePreviewPanel.tsx | 59 ++++++++++ .../[...path]/components/treePreviewPanel.tsx | 107 ++++-------------- .../app/[domain]/browse/[...path]/page.tsx | 48 ++++++-- .../[domain]/browse/hooks/useBrowseParams.ts | 52 ++------- .../src/app/[domain]/browse/hooks/utils.ts | 37 ++++++ 6 files changed, 190 insertions(+), 177 deletions(-) create mode 100644 packages/web/src/app/[domain]/browse/[...path]/components/pureTreePreviewPanel.tsx create mode 100644 packages/web/src/app/[domain]/browse/hooks/utils.ts diff --git a/packages/web/src/app/[domain]/browse/[...path]/components/codePreviewPanel.tsx b/packages/web/src/app/[domain]/browse/[...path]/components/codePreviewPanel.tsx index bedd7aed..82d8f22a 100644 --- a/packages/web/src/app/[domain]/browse/[...path]/components/codePreviewPanel.tsx +++ b/packages/web/src/app/[domain]/browse/[...path]/components/codePreviewPanel.tsx @@ -1,6 +1,4 @@ -'use client'; - -import { getCodeHostInfoForRepo, unwrapServiceError } from "@/lib/utils"; +import { getCodeHostInfoForRepo, isServiceError, unwrapServiceError } from "@/lib/utils"; import { useBrowseParams } from "@/app/[domain]/browse/hooks/useBrowseParams"; import { useQuery } from "@tanstack/react-query"; import { getFileSource } from "@/features/search/fileSourceApi"; @@ -10,54 +8,36 @@ import { Separator } from "@/components/ui/separator"; import { getRepoInfoByName } from "@/actions"; import { cn } from "@/lib/utils"; import Image from "next/image"; -import { useMemo } from "react"; import { PureCodePreviewPanel } from "./pureCodePreviewPanel"; import { PathHeader } from "@/app/[domain]/components/pathHeader"; -export const CodePreviewPanel = () => { - const { path, repoName, revisionName } = useBrowseParams(); - const domain = useDomain(); - - const { data: fileSourceResponse, isPending: isFileSourcePending, isError: isFileSourceError } = useQuery({ - queryKey: ['fileSource', repoName, revisionName, path, domain], - queryFn: () => unwrapServiceError(getFileSource({ - fileName: path, - repository: repoName, - branch: revisionName - }, domain)), - }); - - const { data: repoInfoResponse, isPending: isRepoInfoPending, isError: isRepoInfoError } = useQuery({ - queryKey: ['repoInfo', repoName, domain], - queryFn: () => unwrapServiceError(getRepoInfoByName(repoName, domain)), - }); - - const codeHostInfo = useMemo(() => { - if (!repoInfoResponse) { - return undefined; - } +interface CodePreviewPanelProps { + path: string; + repoName: string; + revisionName?: string; + domain: string; +} - return getCodeHostInfoForRepo({ - codeHostType: repoInfoResponse.codeHostType, - name: repoInfoResponse.name, - displayName: repoInfoResponse.displayName, - webUrl: repoInfoResponse.webUrl, - }); - }, [repoInfoResponse]); +export const CodePreviewPanel = async ({ path, repoName, revisionName, domain }: CodePreviewPanelProps) => { + const fileSourceResponse = await getFileSource({ + fileName: path, + repository: repoName, + branch: revisionName, + }, domain); - if (isFileSourcePending || isRepoInfoPending) { - return ( -
- - Loading... -
- ) - } + const repoInfoResponse = await getRepoInfoByName(repoName, domain); - if (isFileSourceError || isRepoInfoError) { + if (isServiceError(fileSourceResponse) || isServiceError(repoInfoResponse)) { return
Error loading file source
} + const codeHostInfo = getCodeHostInfoForRepo({ + codeHostType: repoInfoResponse.codeHostType, + name: repoInfoResponse.name, + displayName: repoInfoResponse.displayName, + webUrl: repoInfoResponse.webUrl, + }); + return ( <>
diff --git a/packages/web/src/app/[domain]/browse/[...path]/components/pureTreePreviewPanel.tsx b/packages/web/src/app/[domain]/browse/[...path]/components/pureTreePreviewPanel.tsx new file mode 100644 index 00000000..29ccb019 --- /dev/null +++ b/packages/web/src/app/[domain]/browse/[...path]/components/pureTreePreviewPanel.tsx @@ -0,0 +1,59 @@ +'use client'; + +import { useCallback, useRef } from "react"; +import { FileTreeItem } from "@/features/fileTree/actions"; +import { FileTreeItemComponent } from "@/features/fileTree/components/fileTreeItemComponent"; +import { useBrowseNavigation } from "../../hooks/useBrowseNavigation"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { useBrowseParams } from "../../hooks/useBrowseParams"; +import { usePrefetchFileSource } from "@/hooks/usePrefetchFileSource"; +import { usePrefetchFolderContents } from "@/hooks/usePrefetchFolderContents"; + +interface PureTreePreviewPanelProps { + items: FileTreeItem[]; +} + +export const PureTreePreviewPanel = ({ items }: PureTreePreviewPanelProps) => { + const { repoName, revisionName } = useBrowseParams(); + const { navigateToPath } = useBrowseNavigation(); + const { prefetchFileSource } = usePrefetchFileSource(); + const { prefetchFolderContents } = usePrefetchFolderContents(); + const scrollAreaRef = useRef(null); + + const onNodeClicked = useCallback((node: FileTreeItem) => { + navigateToPath({ + repoName: repoName, + revisionName: revisionName, + path: node.path, + pathType: node.type === 'tree' ? 'tree' : 'blob', + }); + }, [navigateToPath, repoName, revisionName]); + + const onNodeMouseEnter = useCallback((node: FileTreeItem) => { + if (node.type === 'blob') { + prefetchFileSource(repoName, revisionName ?? 'HEAD', node.path); + } else if (node.type === 'tree') { + prefetchFolderContents(repoName, revisionName ?? 'HEAD', node.path); + } + }, [prefetchFileSource, prefetchFolderContents, repoName, revisionName]); + + return ( + + {items.map((item) => ( + onNodeClicked(item)} + onMouseEnter={() => onNodeMouseEnter(item)} + parentRef={scrollAreaRef} + /> + ))} + + ) +} \ No newline at end of file diff --git a/packages/web/src/app/[domain]/browse/[...path]/components/treePreviewPanel.tsx b/packages/web/src/app/[domain]/browse/[...path]/components/treePreviewPanel.tsx index 33336ad3..1b4cab21 100644 --- a/packages/web/src/app/[domain]/browse/[...path]/components/treePreviewPanel.tsx +++ b/packages/web/src/app/[domain]/browse/[...path]/components/treePreviewPanel.tsx @@ -1,74 +1,29 @@ -'use client'; -import { Loader2 } from "lucide-react"; import { Separator } from "@/components/ui/separator"; import { getRepoInfoByName } from "@/actions"; import { PathHeader } from "@/app/[domain]/components/pathHeader"; -import { useCallback, useRef } from "react"; -import { FileTreeItem, getFolderContents } from "@/features/fileTree/actions"; -import { FileTreeItemComponent } from "@/features/fileTree/components/fileTreeItemComponent"; -import { useBrowseNavigation } from "../../hooks/useBrowseNavigation"; -import { ScrollArea } from "@/components/ui/scroll-area"; -import { unwrapServiceError } from "@/lib/utils"; -import { useBrowseParams } from "../../hooks/useBrowseParams"; -import { useDomain } from "@/hooks/useDomain"; -import { useQuery } from "@tanstack/react-query"; -import { usePrefetchFileSource } from "@/hooks/usePrefetchFileSource"; -import { usePrefetchFolderContents } from "@/hooks/usePrefetchFolderContents"; - -export const TreePreviewPanel = () => { - const { path } = useBrowseParams(); - const { repoName, revisionName } = useBrowseParams(); - const domain = useDomain(); - const { navigateToPath } = useBrowseNavigation(); - const { prefetchFileSource } = usePrefetchFileSource(); - const { prefetchFolderContents } = usePrefetchFolderContents(); - const scrollAreaRef = useRef(null); - - const { data: repoInfoResponse, isPending: isRepoInfoPending, isError: isRepoInfoError } = useQuery({ - queryKey: ['repoInfo', repoName, domain], - queryFn: () => unwrapServiceError(getRepoInfoByName(repoName, domain)), - }); - - const { data, isPending: isFolderContentsPending, isError: isFolderContentsError } = useQuery({ - queryKey: ['tree', repoName, revisionName, path, domain], - queryFn: () => unwrapServiceError( - getFolderContents({ - repoName, - revisionName: revisionName ?? 'HEAD', - path, - }, domain) - ), - }); - - const onNodeClicked = useCallback((node: FileTreeItem) => { - navigateToPath({ - repoName: repoName, - revisionName: revisionName, - path: node.path, - pathType: node.type === 'tree' ? 'tree' : 'blob', - }); - }, [navigateToPath, repoName, revisionName]); - - const onNodeMouseEnter = useCallback((node: FileTreeItem) => { - if (node.type === 'blob') { - prefetchFileSource(repoName, revisionName ?? 'HEAD', node.path); - } else if (node.type === 'tree') { - prefetchFolderContents(repoName, revisionName ?? 'HEAD', node.path); - } - }, [prefetchFileSource, prefetchFolderContents, repoName, revisionName]); - - if (isFolderContentsPending || isRepoInfoPending) { - return ( -
- - Loading... -
- ) - } - - if (isFolderContentsError || isRepoInfoError) { - return
Error loading tree
+import { getFolderContents } from "@/features/fileTree/actions"; +import { isServiceError } from "@/lib/utils"; +import { PureTreePreviewPanel } from "./pureTreePreviewPanel"; + +interface TreePreviewPanelProps { + path: string; + repoName: string; + revisionName?: string; + domain: string; +} + +export const TreePreviewPanel = async ({ path, repoName, revisionName, domain }: TreePreviewPanelProps) => { + const repoInfoResponse = await getRepoInfoByName(repoName, domain); + + const folderContentsResponse = await getFolderContents({ + repoName, + revisionName: revisionName ?? 'HEAD', + path, + }, domain); + + if (isServiceError(folderContentsResponse) || isServiceError(repoInfoResponse)) { + return
Error loading tree preview
} return ( @@ -86,23 +41,7 @@ export const TreePreviewPanel = () => { />
- - {data.map((item) => ( - onNodeClicked(item)} - onMouseEnter={() => onNodeMouseEnter(item)} - parentRef={scrollAreaRef} - /> - ))} - + ) } \ No newline at end of file diff --git a/packages/web/src/app/[domain]/browse/[...path]/page.tsx b/packages/web/src/app/[domain]/browse/[...path]/page.tsx index 3099f816..9a00b39a 100644 --- a/packages/web/src/app/[domain]/browse/[...path]/page.tsx +++ b/packages/web/src/app/[domain]/browse/[...path]/page.tsx @@ -1,19 +1,47 @@ -'use client'; +// import { CodePreviewPanel } from "./components/codePreviewPanel"; +// import { TreePreviewPanel } from "./components/treePreviewPanel"; -import { useBrowseParams } from "@/app/[domain]/browse/hooks/useBrowseParams"; +import { Suspense } from "react"; +import { getBrowseParamsFromPathParam } from "../hooks/utils"; import { CodePreviewPanel } from "./components/codePreviewPanel"; +import { Loader2 } from "lucide-react"; import { TreePreviewPanel } from "./components/treePreviewPanel"; -export default function BrowsePage() { - const { pathType } = useBrowseParams(); +interface BrowsePageProps { + params: { + path: string[]; + domain: string; + }; +} + +export default async function BrowsePage({ params: { path: _rawPath, domain } }: BrowsePageProps) { + const rawPath = decodeURIComponent(_rawPath.join('/')); + const { repoName, revisionName, path, pathType } = getBrowseParamsFromPathParam(rawPath); + return (
- - {pathType === 'blob' ? ( - - ) : ( - - )} + + + Loading... +
+ }> + {pathType === 'blob' ? ( + + ) : ( + + )} + ) } diff --git a/packages/web/src/app/[domain]/browse/hooks/useBrowseParams.ts b/packages/web/src/app/[domain]/browse/hooks/useBrowseParams.ts index b671d3fc..d7917f72 100644 --- a/packages/web/src/app/[domain]/browse/hooks/useBrowseParams.ts +++ b/packages/web/src/app/[domain]/browse/hooks/useBrowseParams.ts @@ -1,48 +1,18 @@ -'use client'; - import { usePathname } from "next/navigation"; +import { useMemo } from "react"; +import { getBrowseParamsFromPathParam } from "./utils"; export const useBrowseParams = () => { const pathname = usePathname(); - const startIndex = pathname.indexOf('/browse/'); - if (startIndex === -1) { - throw new Error(`Invalid browse pathname: "${pathname}" - expected to contain "/browse/"`); - } - - const rawPath = pathname.substring(startIndex + '/browse/'.length); - const sentinalIndex = rawPath.search(/\/-\/(tree|blob)\//); - if (sentinalIndex === -1) { - throw new Error(`Invalid browse pathname: "${pathname}" - expected to contain "/-/(tree|blob)/" pattern`); - } - - const repoAndRevisionName = rawPath.substring(0, sentinalIndex).split('@'); - const repoName = repoAndRevisionName[0]; - const revisionName = repoAndRevisionName.length > 1 ? repoAndRevisionName[1] : undefined; - - const { path, pathType } = ((): { path: string, pathType: 'tree' | 'blob' } => { - const path = rawPath.substring(sentinalIndex + '/-/'.length); - const pathType = path.startsWith('tree/') ? 'tree' : 'blob'; - - // @note: decodedURIComponent is needed here incase the path contains a space. - switch (pathType) { - case 'tree': - return { - path: decodeURIComponent(path.substring('tree/'.length)), - pathType, - }; - case 'blob': - return { - path: decodeURIComponent(path.substring('blob/'.length)), - pathType, - }; + return useMemo(() => { + const startIndex = pathname.indexOf('/browse/'); + if (startIndex === -1) { + throw new Error(`Invalid browse pathname: "${pathname}" - expected to contain "/browse/"`); } - })(); - return { - repoName, - revisionName, - path, - pathType, - } -} \ No newline at end of file + const rawPath = pathname.substring(startIndex + '/browse/'.length); + return getBrowseParamsFromPathParam(rawPath); + }, [pathname]); +} + diff --git a/packages/web/src/app/[domain]/browse/hooks/utils.ts b/packages/web/src/app/[domain]/browse/hooks/utils.ts new file mode 100644 index 00000000..a0e423be --- /dev/null +++ b/packages/web/src/app/[domain]/browse/hooks/utils.ts @@ -0,0 +1,37 @@ + +export const getBrowseParamsFromPathParam = (pathParam: string) => { + const sentinalIndex = pathParam.search(/\/-\/(tree|blob)\//); + if (sentinalIndex === -1) { + throw new Error(`Invalid browse pathname: "${pathParam}" - expected to contain "/-/(tree|blob)/" pattern`); + } + + const repoAndRevisionName = pathParam.substring(0, sentinalIndex).split('@'); + const repoName = repoAndRevisionName[0]; + const revisionName = repoAndRevisionName.length > 1 ? repoAndRevisionName[1] : undefined; + + const { path, pathType } = ((): { path: string, pathType: 'tree' | 'blob' } => { + const path = pathParam.substring(sentinalIndex + '/-/'.length); + const pathType = path.startsWith('tree/') ? 'tree' : 'blob'; + + // @note: decodedURIComponent is needed here incase the path contains a space. + switch (pathType) { + case 'tree': + return { + path: decodeURIComponent(path.substring('tree/'.length)), + pathType, + }; + case 'blob': + return { + path: decodeURIComponent(path.substring('blob/'.length)), + pathType, + }; + } + })(); + + return { + repoName, + revisionName, + path, + pathType, + } +} \ No newline at end of file From 5f0bd5a3fa3e2cdd10cb06a1af6a0acbe8327396 Mon Sep 17 00:00:00 2001 From: bkellam Date: Tue, 17 Jun 2025 15:05:48 -0700 Subject: [PATCH 2/7] Add useBrowsePath --- .../browse/hooks/useBrowseNavigation.ts | 61 +++++++++++++------ .../[domain]/browse/hooks/useBrowsePath.ts | 32 ++++++++++ 2 files changed, 75 insertions(+), 18 deletions(-) create mode 100644 packages/web/src/app/[domain]/browse/hooks/useBrowsePath.ts diff --git a/packages/web/src/app/[domain]/browse/hooks/useBrowseNavigation.ts b/packages/web/src/app/[domain]/browse/hooks/useBrowseNavigation.ts index 83780153..372e31f6 100644 --- a/packages/web/src/app/[domain]/browse/hooks/useBrowseNavigation.ts +++ b/packages/web/src/app/[domain]/browse/hooks/useBrowseNavigation.ts @@ -13,15 +13,46 @@ export type BrowseHighlightRange = { export const HIGHLIGHT_RANGE_QUERY_PARAM = 'highlightRange'; -interface NavigateToPathOptions { +export interface GetBrowsePathProps { repoName: string; revisionName?: string; path: string; pathType: 'blob' | 'tree'; highlightRange?: BrowseHighlightRange; setBrowseState?: Partial; + domain: string; } +export const getBrowsePath = ({ + repoName, + revisionName = 'HEAD', + path, + pathType, + highlightRange, + setBrowseState, + domain, +}: GetBrowsePathProps) => { + const params = new URLSearchParams(); + + if (highlightRange) { + const { start, end } = highlightRange; + + if ('column' in start && 'column' in end) { + params.set(HIGHLIGHT_RANGE_QUERY_PARAM, `${start.lineNumber}:${start.column},${end.lineNumber}:${end.column}`); + } else { + params.set(HIGHLIGHT_RANGE_QUERY_PARAM, `${start.lineNumber},${end.lineNumber}`); + } + } + + if (setBrowseState) { + params.set(SET_BROWSE_STATE_QUERY_PARAM, JSON.stringify(setBrowseState)); + } + + const browsePath = `/${domain}/browse/${repoName}@${revisionName}/-/${pathType}/${path}${params.keys.length > 0 ? `?${params.toString()}` : ''}`; + return browsePath; +} + + export const useBrowseNavigation = () => { const router = useRouter(); const domain = useDomain(); @@ -33,24 +64,18 @@ export const useBrowseNavigation = () => { pathType, highlightRange, setBrowseState, - }: NavigateToPathOptions) => { - const params = new URLSearchParams(); - - if (highlightRange) { - const { start, end } = highlightRange; - - if ('column' in start && 'column' in end) { - params.set(HIGHLIGHT_RANGE_QUERY_PARAM, `${start.lineNumber}:${start.column},${end.lineNumber}:${end.column}`); - } else { - params.set(HIGHLIGHT_RANGE_QUERY_PARAM, `${start.lineNumber},${end.lineNumber}`); - } - } - - if (setBrowseState) { - params.set(SET_BROWSE_STATE_QUERY_PARAM, JSON.stringify(setBrowseState)); - } + }: Omit) => { + const browsePath = getBrowsePath({ + repoName, + revisionName, + path, + pathType, + highlightRange, + setBrowseState, + domain, + }); - router.push(`/${domain}/browse/${repoName}@${revisionName}/-/${pathType}/${path}?${params.toString()}`); + router.push(browsePath); }, [domain, router]); return { diff --git a/packages/web/src/app/[domain]/browse/hooks/useBrowsePath.ts b/packages/web/src/app/[domain]/browse/hooks/useBrowsePath.ts new file mode 100644 index 00000000..fcf29be8 --- /dev/null +++ b/packages/web/src/app/[domain]/browse/hooks/useBrowsePath.ts @@ -0,0 +1,32 @@ +'use client'; + +import { useMemo } from "react"; +import { getBrowsePath, GetBrowsePathProps } from "./useBrowseNavigation"; +import { useDomain } from "@/hooks/useDomain"; + +export const useBrowsePath = ({ + repoName, + revisionName, + path, + pathType, + highlightRange, + setBrowseState, +}: Omit) => { + const domain = useDomain(); + + const browsePath = useMemo(() => { + return getBrowsePath({ + repoName, + revisionName, + path, + pathType, + highlightRange, + setBrowseState, + domain, + }); + }, [repoName, revisionName, path, pathType, highlightRange, setBrowseState, domain]); + + return { + path: browsePath, + } +} \ No newline at end of file From ef304f0af6a3b0fcc6bef0c0e58c548f168f711e Mon Sep 17 00:00:00 2001 From: bkellam Date: Tue, 24 Jun 2025 16:40:35 -0700 Subject: [PATCH 3/7] Revert "Add useBrowsePath" This reverts commit 5f0bd5a3fa3e2cdd10cb06a1af6a0acbe8327396. --- .../browse/hooks/useBrowseNavigation.ts | 61 ++++++------------- .../[domain]/browse/hooks/useBrowsePath.ts | 32 ---------- 2 files changed, 18 insertions(+), 75 deletions(-) delete mode 100644 packages/web/src/app/[domain]/browse/hooks/useBrowsePath.ts diff --git a/packages/web/src/app/[domain]/browse/hooks/useBrowseNavigation.ts b/packages/web/src/app/[domain]/browse/hooks/useBrowseNavigation.ts index 372e31f6..83780153 100644 --- a/packages/web/src/app/[domain]/browse/hooks/useBrowseNavigation.ts +++ b/packages/web/src/app/[domain]/browse/hooks/useBrowseNavigation.ts @@ -13,46 +13,15 @@ export type BrowseHighlightRange = { export const HIGHLIGHT_RANGE_QUERY_PARAM = 'highlightRange'; -export interface GetBrowsePathProps { +interface NavigateToPathOptions { repoName: string; revisionName?: string; path: string; pathType: 'blob' | 'tree'; highlightRange?: BrowseHighlightRange; setBrowseState?: Partial; - domain: string; } -export const getBrowsePath = ({ - repoName, - revisionName = 'HEAD', - path, - pathType, - highlightRange, - setBrowseState, - domain, -}: GetBrowsePathProps) => { - const params = new URLSearchParams(); - - if (highlightRange) { - const { start, end } = highlightRange; - - if ('column' in start && 'column' in end) { - params.set(HIGHLIGHT_RANGE_QUERY_PARAM, `${start.lineNumber}:${start.column},${end.lineNumber}:${end.column}`); - } else { - params.set(HIGHLIGHT_RANGE_QUERY_PARAM, `${start.lineNumber},${end.lineNumber}`); - } - } - - if (setBrowseState) { - params.set(SET_BROWSE_STATE_QUERY_PARAM, JSON.stringify(setBrowseState)); - } - - const browsePath = `/${domain}/browse/${repoName}@${revisionName}/-/${pathType}/${path}${params.keys.length > 0 ? `?${params.toString()}` : ''}`; - return browsePath; -} - - export const useBrowseNavigation = () => { const router = useRouter(); const domain = useDomain(); @@ -64,18 +33,24 @@ export const useBrowseNavigation = () => { pathType, highlightRange, setBrowseState, - }: Omit) => { - const browsePath = getBrowsePath({ - repoName, - revisionName, - path, - pathType, - highlightRange, - setBrowseState, - domain, - }); + }: NavigateToPathOptions) => { + const params = new URLSearchParams(); + + if (highlightRange) { + const { start, end } = highlightRange; + + if ('column' in start && 'column' in end) { + params.set(HIGHLIGHT_RANGE_QUERY_PARAM, `${start.lineNumber}:${start.column},${end.lineNumber}:${end.column}`); + } else { + params.set(HIGHLIGHT_RANGE_QUERY_PARAM, `${start.lineNumber},${end.lineNumber}`); + } + } + + if (setBrowseState) { + params.set(SET_BROWSE_STATE_QUERY_PARAM, JSON.stringify(setBrowseState)); + } - router.push(browsePath); + router.push(`/${domain}/browse/${repoName}@${revisionName}/-/${pathType}/${path}?${params.toString()}`); }, [domain, router]); return { diff --git a/packages/web/src/app/[domain]/browse/hooks/useBrowsePath.ts b/packages/web/src/app/[domain]/browse/hooks/useBrowsePath.ts deleted file mode 100644 index fcf29be8..00000000 --- a/packages/web/src/app/[domain]/browse/hooks/useBrowsePath.ts +++ /dev/null @@ -1,32 +0,0 @@ -'use client'; - -import { useMemo } from "react"; -import { getBrowsePath, GetBrowsePathProps } from "./useBrowseNavigation"; -import { useDomain } from "@/hooks/useDomain"; - -export const useBrowsePath = ({ - repoName, - revisionName, - path, - pathType, - highlightRange, - setBrowseState, -}: Omit) => { - const domain = useDomain(); - - const browsePath = useMemo(() => { - return getBrowsePath({ - repoName, - revisionName, - path, - pathType, - highlightRange, - setBrowseState, - domain, - }); - }, [repoName, revisionName, path, pathType, highlightRange, setBrowseState, domain]); - - return { - path: browsePath, - } -} \ No newline at end of file From e188aa8787fe5823945a2a553a15c6a4d12eaade Mon Sep 17 00:00:00 2001 From: bkellam Date: Tue, 24 Jun 2025 16:40:57 -0700 Subject: [PATCH 4/7] remove dead code --- packages/web/src/app/[domain]/browse/[...path]/page.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/web/src/app/[domain]/browse/[...path]/page.tsx b/packages/web/src/app/[domain]/browse/[...path]/page.tsx index 9a00b39a..12689f86 100644 --- a/packages/web/src/app/[domain]/browse/[...path]/page.tsx +++ b/packages/web/src/app/[domain]/browse/[...path]/page.tsx @@ -1,6 +1,3 @@ -// import { CodePreviewPanel } from "./components/codePreviewPanel"; -// import { TreePreviewPanel } from "./components/treePreviewPanel"; - import { Suspense } from "react"; import { getBrowseParamsFromPathParam } from "../hooks/utils"; import { CodePreviewPanel } from "./components/codePreviewPanel"; From 27004e15edf76809a0ebb9474a8a48ce684097a7 Mon Sep 17 00:00:00 2001 From: bkellam Date: Tue, 24 Jun 2025 16:46:41 -0700 Subject: [PATCH 5/7] remove explicit prefetching --- .../components/pureTreePreviewPanel.tsx | 13 ------- .../components/fileSearchCommandDialog.tsx | 15 -------- .../app/[domain]/components/pathHeader.tsx | 19 ---------- .../components/exploreMenu/referenceList.tsx | 12 ------- .../components/fileTreeItemComponent.tsx | 3 -- .../fileTree/components/pureFileTreePanel.tsx | 17 +-------- .../web/src/hooks/usePrefetchFileSource.ts | 34 ------------------ .../src/hooks/usePrefetchFolderContents.ts | 36 ------------------- 8 files changed, 1 insertion(+), 148 deletions(-) delete mode 100644 packages/web/src/hooks/usePrefetchFileSource.ts delete mode 100644 packages/web/src/hooks/usePrefetchFolderContents.ts diff --git a/packages/web/src/app/[domain]/browse/[...path]/components/pureTreePreviewPanel.tsx b/packages/web/src/app/[domain]/browse/[...path]/components/pureTreePreviewPanel.tsx index 29ccb019..17860185 100644 --- a/packages/web/src/app/[domain]/browse/[...path]/components/pureTreePreviewPanel.tsx +++ b/packages/web/src/app/[domain]/browse/[...path]/components/pureTreePreviewPanel.tsx @@ -6,8 +6,6 @@ import { FileTreeItemComponent } from "@/features/fileTree/components/fileTreeIt import { useBrowseNavigation } from "../../hooks/useBrowseNavigation"; import { ScrollArea } from "@/components/ui/scroll-area"; import { useBrowseParams } from "../../hooks/useBrowseParams"; -import { usePrefetchFileSource } from "@/hooks/usePrefetchFileSource"; -import { usePrefetchFolderContents } from "@/hooks/usePrefetchFolderContents"; interface PureTreePreviewPanelProps { items: FileTreeItem[]; @@ -16,8 +14,6 @@ interface PureTreePreviewPanelProps { export const PureTreePreviewPanel = ({ items }: PureTreePreviewPanelProps) => { const { repoName, revisionName } = useBrowseParams(); const { navigateToPath } = useBrowseNavigation(); - const { prefetchFileSource } = usePrefetchFileSource(); - const { prefetchFolderContents } = usePrefetchFolderContents(); const scrollAreaRef = useRef(null); const onNodeClicked = useCallback((node: FileTreeItem) => { @@ -29,14 +25,6 @@ export const PureTreePreviewPanel = ({ items }: PureTreePreviewPanelProps) => { }); }, [navigateToPath, repoName, revisionName]); - const onNodeMouseEnter = useCallback((node: FileTreeItem) => { - if (node.type === 'blob') { - prefetchFileSource(repoName, revisionName ?? 'HEAD', node.path); - } else if (node.type === 'tree') { - prefetchFolderContents(repoName, revisionName ?? 'HEAD', node.path); - } - }, [prefetchFileSource, prefetchFolderContents, repoName, revisionName]); - return ( { depth={0} isCollapseChevronVisible={false} onClick={() => onNodeClicked(item)} - onMouseEnter={() => onNodeMouseEnter(item)} parentRef={scrollAreaRef} /> ))} diff --git a/packages/web/src/app/[domain]/browse/components/fileSearchCommandDialog.tsx b/packages/web/src/app/[domain]/browse/components/fileSearchCommandDialog.tsx index 27e0261f..d87eab85 100644 --- a/packages/web/src/app/[domain]/browse/components/fileSearchCommandDialog.tsx +++ b/packages/web/src/app/[domain]/browse/components/fileSearchCommandDialog.tsx @@ -10,7 +10,6 @@ import { useDomain } from "@/hooks/useDomain"; import { Dialog, DialogContent, DialogDescription, DialogTitle } from "@/components/ui/dialog"; import { useBrowseNavigation } from "../hooks/useBrowseNavigation"; import { useBrowseState } from "../hooks/useBrowseState"; -import { usePrefetchFileSource } from "@/hooks/usePrefetchFileSource"; import { useBrowseParams } from "../hooks/useBrowseParams"; import { FileTreeItemIcon } from "@/features/fileTree/components/fileTreeItemIcon"; import { useLocalStorage } from "usehooks-ts"; @@ -36,7 +35,6 @@ export const FileSearchCommandDialog = () => { const inputRef = useRef(null); const [searchQuery, setSearchQuery] = useState(''); const { navigateToPath } = useBrowseNavigation(); - const { prefetchFileSource } = usePrefetchFileSource(); const [recentlyOpened, setRecentlyOpened] = useLocalStorage(`recentlyOpenedFiles-${repoName}`, []); @@ -122,14 +120,6 @@ export const FileSearchCommandDialog = () => { }); }, [navigateToPath, repoName, revisionName, setRecentlyOpened, updateBrowseState]); - const onMouseEnter = useCallback((file: FileTreeItem) => { - prefetchFileSource( - repoName, - revisionName ?? 'HEAD', - file.path - ); - }, [prefetchFileSource, repoName, revisionName]); - // @note: We were hitting issues when the user types into the input field while the files are still // loading. The workaround was to set `disabled` when loading and then focus the input field when // the files are loaded, hence the `useEffect` below. @@ -181,7 +171,6 @@ export const FileSearchCommandDialog = () => { key={file.path} file={file} onSelect={() => onSelect(file)} - onMouseEnter={() => onMouseEnter(file)} /> ); })} @@ -196,7 +185,6 @@ export const FileSearchCommandDialog = () => { file={file} match={match} onSelect={() => onSelect(file)} - onMouseEnter={() => onMouseEnter(file)} /> ); })} @@ -223,20 +211,17 @@ interface SearchResultComponentProps { to: number; }; onSelect: () => void; - onMouseEnter: () => void; } const SearchResultComponent = ({ file, match, onSelect, - onMouseEnter, }: SearchResultComponentProps) => { return (
diff --git a/packages/web/src/app/[domain]/components/pathHeader.tsx b/packages/web/src/app/[domain]/components/pathHeader.tsx index d4fd1e74..5118392f 100644 --- a/packages/web/src/app/[domain]/components/pathHeader.tsx +++ b/packages/web/src/app/[domain]/components/pathHeader.tsx @@ -8,8 +8,6 @@ import { useBrowseNavigation } from "../browse/hooks/useBrowseNavigation"; import { Copy, CheckCircle2, ChevronRight, MoreHorizontal } from "lucide-react"; import { useCallback, useState, useMemo, useRef, useEffect } from "react"; import { useToast } from "@/components/hooks/use-toast"; -import { usePrefetchFolderContents } from "@/hooks/usePrefetchFolderContents"; -import { usePrefetchFileSource } from "@/hooks/usePrefetchFileSource"; import { DropdownMenu, DropdownMenuContent, @@ -62,8 +60,6 @@ export const PathHeader = ({ const { navigateToPath } = useBrowseNavigation(); const { toast } = useToast(); const [copied, setCopied] = useState(false); - const { prefetchFolderContents } = usePrefetchFolderContents(); - const { prefetchFileSource } = usePrefetchFileSource(); const containerRef = useRef(null); const breadcrumbsRef = useRef(null); @@ -188,19 +184,6 @@ export const PathHeader = ({ }); }, [repo.name, branchDisplayName, navigateToPath, pathType]); - const onBreadcrumbMouseEnter = useCallback((segment: BreadcrumbSegment) => { - if (segment.isLastSegment && pathType === 'blob') { - prefetchFileSource(repo.name, branchDisplayName ?? 'HEAD', segment.fullPath); - } else { - prefetchFolderContents(repo.name, branchDisplayName ?? 'HEAD', segment.fullPath); - } - }, [ - repo.name, - branchDisplayName, - prefetchFolderContents, - pathType, - prefetchFileSource, - ]); const renderSegmentWithHighlight = (segment: BreadcrumbSegment) => { if (!segment.highlightRange) { @@ -274,7 +257,6 @@ export const PathHeader = ({ onBreadcrumbClick(segment)} - onMouseEnter={() => onBreadcrumbMouseEnter(segment)} className="font-mono text-sm cursor-pointer" > {renderSegmentWithHighlight(segment)} @@ -292,7 +274,6 @@ export const PathHeader = ({ "font-mono text-sm truncate cursor-pointer hover:underline", )} onClick={() => onBreadcrumbClick(segment)} - onMouseEnter={() => onBreadcrumbMouseEnter(segment)} > {renderSegmentWithHighlight(segment)} diff --git a/packages/web/src/ee/features/codeNav/components/exploreMenu/referenceList.tsx b/packages/web/src/ee/features/codeNav/components/exploreMenu/referenceList.tsx index b55edc3f..0458ecbc 100644 --- a/packages/web/src/ee/features/codeNav/components/exploreMenu/referenceList.tsx +++ b/packages/web/src/ee/features/codeNav/components/exploreMenu/referenceList.tsx @@ -8,7 +8,6 @@ import { RepositoryInfo, SourceRange } from "@/features/search/types"; import { useMemo, useRef } from "react"; import useCaptureEvent from "@/hooks/useCaptureEvent"; import { useVirtualizer } from "@tanstack/react-virtual"; -import { usePrefetchFileSource } from "@/hooks/usePrefetchFileSource"; interface ReferenceListProps { data: FindRelatedSymbolsResponse; @@ -31,7 +30,6 @@ export const ReferenceList = ({ const { navigateToPath } = useBrowseNavigation(); const captureEvent = useCaptureEvent(); - const { prefetchFileSource } = usePrefetchFileSource(); // Virtualization setup const parentRef = useRef(null); @@ -120,13 +118,6 @@ export const ReferenceList = ({ highlightRange: match.range, }) }} - // @note: We prefetch the file source when the user hovers over a file. - // This is to try and mitigate having a loading spinner appear when - // the user clicks on a file to open it. - // @see: /browse/[...path]/page.tsx - onMouseEnter={() => { - prefetchFileSource(file.repository, revisionName, file.fileName); - }} /> ))}
@@ -144,7 +135,6 @@ interface ReferenceListItemProps { range: SourceRange; language: string; onClick: () => void; - onMouseEnter: () => void; } const ReferenceListItem = ({ @@ -152,7 +142,6 @@ const ReferenceListItem = ({ range, language, onClick, - onMouseEnter, }: ReferenceListItemProps) => { const highlightRanges = useMemo(() => [range], [range]); @@ -160,7 +149,6 @@ const ReferenceListItem = ({
void, - onMouseEnter: () => void, parentRef: React.RefObject, }) => { const ref = useRef(null); @@ -67,7 +65,6 @@ export const FileTreeItemComponent = ({ } }} onClick={onClick} - onMouseEnter={onMouseEnter} >
& { @@ -44,7 +43,6 @@ export const PureFileTreePanel = ({ tree: _tree, path }: PureFileTreePanelProps) const scrollAreaRef = useRef(null); const { navigateToPath } = useBrowseNavigation(); const { repoName, revisionName } = useBrowseParams(); - const { prefetchFileSource } = usePrefetchFileSource(); // @note: When `_tree` changes, it indicates that a new tree has been loaded. // In that case, we need to rebuild the collapsable tree. @@ -89,18 +87,6 @@ export const PureFileTreePanel = ({ tree: _tree, path }: PureFileTreePanelProps) } }, [setIsCollapsed, navigateToPath, repoName, revisionName]); - // @note: We prefetch the file source when the user hovers over a file. - // This is to try and mitigate having a loading spinner appear when - // the user clicks on a file to open it. - // @see: /browse/[...path]/page.tsx - const onNodeMouseEnter = useCallback((node: FileTreeNode) => { - if (node.type !== 'blob') { - return; - } - - prefetchFileSource(repoName, revisionName ?? 'HEAD', node.path); - }, [prefetchFileSource, repoName, revisionName]); - const renderTree = useCallback((nodes: FileTreeNode, depth = 0): React.ReactNode => { return ( <> @@ -115,7 +101,6 @@ export const PureFileTreePanel = ({ tree: _tree, path }: PureFileTreePanelProps) isCollapsed={node.isCollapsed} isCollapseChevronVisible={node.type === 'tree'} onClick={() => onNodeClicked(node)} - onMouseEnter={() => onNodeMouseEnter(node)} parentRef={scrollAreaRef} /> {node.children.length > 0 && !node.isCollapsed && renderTree(node, depth + 1)} @@ -124,7 +109,7 @@ export const PureFileTreePanel = ({ tree: _tree, path }: PureFileTreePanelProps) })} ); - }, [path, onNodeClicked, onNodeMouseEnter]); + }, [path, onNodeClicked]); const renderedTree = useMemo(() => renderTree(tree), [tree, renderTree]); diff --git a/packages/web/src/hooks/usePrefetchFileSource.ts b/packages/web/src/hooks/usePrefetchFileSource.ts deleted file mode 100644 index de9cce34..00000000 --- a/packages/web/src/hooks/usePrefetchFileSource.ts +++ /dev/null @@ -1,34 +0,0 @@ -'use client'; - -import { useQueryClient } from "@tanstack/react-query"; -import { useDomain } from "./useDomain"; -import { unwrapServiceError } from "@/lib/utils"; -import { getFileSource } from "@/features/search/fileSourceApi"; -import { useDebounceCallback } from "usehooks-ts"; - -interface UsePrefetchFileSourceProps { - debounceDelay?: number; - staleTime?: number; -} - -export const usePrefetchFileSource = ({ - debounceDelay = 200, - staleTime = 5 * 60 * 1000, // 5 minutes -}: UsePrefetchFileSourceProps = {}) => { - const queryClient = useQueryClient(); - const domain = useDomain(); - - const prefetchFileSource = useDebounceCallback((repoName: string, revisionName: string, path: string) => { - queryClient.prefetchQuery({ - queryKey: ['fileSource', repoName, revisionName, path, domain], - queryFn: () => unwrapServiceError(getFileSource({ - fileName: path, - repository: repoName, - branch: revisionName, - }, domain)), - staleTime, - }); - }, debounceDelay); - - return { prefetchFileSource }; -} \ No newline at end of file diff --git a/packages/web/src/hooks/usePrefetchFolderContents.ts b/packages/web/src/hooks/usePrefetchFolderContents.ts deleted file mode 100644 index e135cb7a..00000000 --- a/packages/web/src/hooks/usePrefetchFolderContents.ts +++ /dev/null @@ -1,36 +0,0 @@ -'use client'; - -import { useQueryClient } from "@tanstack/react-query"; -import { useDomain } from "./useDomain"; -import { unwrapServiceError } from "@/lib/utils"; -import { getFolderContents } from "@/features/fileTree/actions"; -import { useDebounceCallback } from "usehooks-ts"; - -interface UsePrefetchFolderContentsProps { - debounceDelay?: number; - staleTime?: number; -} - -export const usePrefetchFolderContents = ({ - debounceDelay = 200, - staleTime = 5 * 60 * 1000, // 5 minutes -}: UsePrefetchFolderContentsProps = {}) => { - const queryClient = useQueryClient(); - const domain = useDomain(); - - const prefetchFolderContents = useDebounceCallback((repoName: string, revisionName: string, path: string) => { - queryClient.prefetchQuery({ - queryKey: ['tree', repoName, revisionName, path, domain], - queryFn: () => unwrapServiceError( - getFolderContents({ - repoName, - revisionName, - path, - }, domain) - ), - staleTime, - }); - }, debounceDelay); - - return { prefetchFolderContents }; -} \ No newline at end of file From f8ef1a893f28ba5d71b82735ef2a206253e58ad9 Mon Sep 17 00:00:00 2001 From: bkellam Date: Tue, 24 Jun 2025 17:48:24 -0700 Subject: [PATCH 6/7] changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a121f0de..af677fb1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Fixed issue with external source code links being broken for paths with spaces. [#364](https://github.com/sourcebot-dev/sourcebot/pull/364) +- Fixed issue where files would sometimes never load in the code browser. [#365](https://github.com/sourcebot-dev/sourcebot/pull/365) ## [4.5.0] - 2025-06-21 From f35cf4759bdbdaf91305ced0952c33e066bd0054 Mon Sep 17 00:00:00 2001 From: bkellam Date: Tue, 24 Jun 2025 18:42:18 -0700 Subject: [PATCH 7/7] feedback --- .../[...path]/components/codePreviewPanel.tsx | 28 ++++++++----------- .../[...path]/components/treePreviewPanel.tsx | 15 +++++----- 2 files changed, 20 insertions(+), 23 deletions(-) diff --git a/packages/web/src/app/[domain]/browse/[...path]/components/codePreviewPanel.tsx b/packages/web/src/app/[domain]/browse/[...path]/components/codePreviewPanel.tsx index 82d8f22a..c25f9a33 100644 --- a/packages/web/src/app/[domain]/browse/[...path]/components/codePreviewPanel.tsx +++ b/packages/web/src/app/[domain]/browse/[...path]/components/codePreviewPanel.tsx @@ -1,15 +1,10 @@ -import { getCodeHostInfoForRepo, isServiceError, unwrapServiceError } from "@/lib/utils"; -import { useBrowseParams } from "@/app/[domain]/browse/hooks/useBrowseParams"; -import { useQuery } from "@tanstack/react-query"; -import { getFileSource } from "@/features/search/fileSourceApi"; -import { useDomain } from "@/hooks/useDomain"; -import { Loader2 } from "lucide-react"; -import { Separator } from "@/components/ui/separator"; import { getRepoInfoByName } from "@/actions"; -import { cn } from "@/lib/utils"; +import { PathHeader } from "@/app/[domain]/components/pathHeader"; +import { Separator } from "@/components/ui/separator"; +import { getFileSource } from "@/features/search/fileSourceApi"; +import { cn, getCodeHostInfoForRepo, isServiceError } from "@/lib/utils"; import Image from "next/image"; import { PureCodePreviewPanel } from "./pureCodePreviewPanel"; -import { PathHeader } from "@/app/[domain]/components/pathHeader"; interface CodePreviewPanelProps { path: string; @@ -19,13 +14,14 @@ interface CodePreviewPanelProps { } export const CodePreviewPanel = async ({ path, repoName, revisionName, domain }: CodePreviewPanelProps) => { - const fileSourceResponse = await getFileSource({ - fileName: path, - repository: repoName, - branch: revisionName, - }, domain); - - const repoInfoResponse = await getRepoInfoByName(repoName, domain); + const [fileSourceResponse, repoInfoResponse] = await Promise.all([ + getFileSource({ + fileName: path, + repository: repoName, + branch: revisionName, + }, domain), + getRepoInfoByName(repoName, domain), + ]); if (isServiceError(fileSourceResponse) || isServiceError(repoInfoResponse)) { return
Error loading file source
diff --git a/packages/web/src/app/[domain]/browse/[...path]/components/treePreviewPanel.tsx b/packages/web/src/app/[domain]/browse/[...path]/components/treePreviewPanel.tsx index 1b4cab21..f2484507 100644 --- a/packages/web/src/app/[domain]/browse/[...path]/components/treePreviewPanel.tsx +++ b/packages/web/src/app/[domain]/browse/[...path]/components/treePreviewPanel.tsx @@ -14,13 +14,14 @@ interface TreePreviewPanelProps { } export const TreePreviewPanel = async ({ path, repoName, revisionName, domain }: TreePreviewPanelProps) => { - const repoInfoResponse = await getRepoInfoByName(repoName, domain); - - const folderContentsResponse = await getFolderContents({ - repoName, - revisionName: revisionName ?? 'HEAD', - path, - }, domain); + const [repoInfoResponse, folderContentsResponse] = await Promise.all([ + getRepoInfoByName(repoName, domain), + getFolderContents({ + repoName, + revisionName: revisionName ?? 'HEAD', + path, + }, domain) + ]); if (isServiceError(folderContentsResponse) || isServiceError(repoInfoResponse)) { return
Error loading tree preview