Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,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)
- Makes base retry indexing configuration configurable and move from a default of `5s` to `60s`. [#377](https://github.com/sourcebot-dev/sourcebot/pull/377)
- 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

Expand Down
Original file line number Diff line number Diff line change
@@ -1,63 +1,39 @@
'use client';

import { getCodeHostInfoForRepo, 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 { 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();
interface CodePreviewPanelProps {
path: string;
repoName: string;
revisionName?: string;
domain: string;
}

const { data: fileSourceResponse, isPending: isFileSourcePending, isError: isFileSourceError } = useQuery({
queryKey: ['fileSource', repoName, revisionName, path, domain],
queryFn: () => unwrapServiceError(getFileSource({
export const CodePreviewPanel = async ({ path, repoName, revisionName, domain }: CodePreviewPanelProps) => {
const [fileSourceResponse, repoInfoResponse] = await Promise.all([
getFileSource({
fileName: path,
repository: repoName,
branch: revisionName
}, domain)),
});
branch: revisionName,
}, domain),
getRepoInfoByName(repoName, 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;
}

return getCodeHostInfoForRepo({
codeHostType: repoInfoResponse.codeHostType,
name: repoInfoResponse.name,
displayName: repoInfoResponse.displayName,
webUrl: repoInfoResponse.webUrl,
});
}, [repoInfoResponse]);

if (isFileSourcePending || isRepoInfoPending) {
return (
<div className="flex flex-col w-full min-h-full items-center justify-center">
<Loader2 className="w-4 h-4 animate-spin" />
Loading...
</div>
)
}

if (isFileSourceError || isRepoInfoError) {
if (isServiceError(fileSourceResponse) || isServiceError(repoInfoResponse)) {
return <div>Error loading file source</div>
}

const codeHostInfo = getCodeHostInfoForRepo({
codeHostType: repoInfoResponse.codeHostType,
name: repoInfoResponse.name,
displayName: repoInfoResponse.displayName,
webUrl: repoInfoResponse.webUrl,
});

return (
<>
<div className="flex flex-row py-1 px-2 items-center justify-between">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
'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";

interface PureTreePreviewPanelProps {
items: FileTreeItem[];
}

export const PureTreePreviewPanel = ({ items }: PureTreePreviewPanelProps) => {
const { repoName, revisionName } = useBrowseParams();
const { navigateToPath } = useBrowseNavigation();
const scrollAreaRef = useRef<HTMLDivElement>(null);

const onNodeClicked = useCallback((node: FileTreeItem) => {
navigateToPath({
repoName: repoName,
revisionName: revisionName,
path: node.path,
pathType: node.type === 'tree' ? 'tree' : 'blob',
});
}, [navigateToPath, repoName, revisionName]);

return (
<ScrollArea
className="flex flex-col p-0.5"
ref={scrollAreaRef}
>
{items.map((item) => (
<FileTreeItemComponent
key={item.path}
node={item}
isActive={false}
depth={0}
isCollapseChevronVisible={false}
onClick={() => onNodeClicked(item)}
parentRef={scrollAreaRef}
/>
))}
</ScrollArea>
)
}
Original file line number Diff line number Diff line change
@@ -1,74 +1,30 @@
'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<HTMLDivElement>(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 (
<div className="flex flex-col w-full min-h-full items-center justify-center">
<Loader2 className="w-4 h-4 animate-spin" />
Loading...
</div>
)
}

if (isFolderContentsError || isRepoInfoError) {
return <div>Error loading tree</div>
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, folderContentsResponse] = await Promise.all([
getRepoInfoByName(repoName, domain),
getFolderContents({
repoName,
revisionName: revisionName ?? 'HEAD',
path,
}, domain)
]);

if (isServiceError(folderContentsResponse) || isServiceError(repoInfoResponse)) {
return <div>Error loading tree preview</div>
}

return (
Expand All @@ -86,23 +42,7 @@ export const TreePreviewPanel = () => {
/>
</div>
<Separator />
<ScrollArea
className="flex flex-col p-0.5"
ref={scrollAreaRef}
>
{data.map((item) => (
<FileTreeItemComponent
key={item.path}
node={item}
isActive={false}
depth={0}
isCollapseChevronVisible={false}
onClick={() => onNodeClicked(item)}
onMouseEnter={() => onNodeMouseEnter(item)}
parentRef={scrollAreaRef}
/>
))}
</ScrollArea>
<PureTreePreviewPanel items={folderContentsResponse} />
</>
)
}
47 changes: 36 additions & 11 deletions packages/web/src/app/[domain]/browse/[...path]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,44 @@
'use client';

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 (
<div className="flex flex-col h-full">

{pathType === 'blob' ? (
<CodePreviewPanel />
) : (
<TreePreviewPanel />
)}
<Suspense fallback={
<div className="flex flex-col w-full min-h-full items-center justify-center">
<Loader2 className="w-4 h-4 animate-spin" />
Loading...
</div>
}>
{pathType === 'blob' ? (
<CodePreviewPanel
path={path}
repoName={repoName}
revisionName={revisionName}
domain={domain}
/>
) : (
<TreePreviewPanel
path={path}
repoName={repoName}
revisionName={revisionName}
domain={domain}
/>
)}
</Suspense>
</div>
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -36,7 +35,6 @@ export const FileSearchCommandDialog = () => {
const inputRef = useRef<HTMLInputElement>(null);
const [searchQuery, setSearchQuery] = useState('');
const { navigateToPath } = useBrowseNavigation();
const { prefetchFileSource } = usePrefetchFileSource();

const [recentlyOpened, setRecentlyOpened] = useLocalStorage<FileTreeItem[]>(`recentlyOpenedFiles-${repoName}`, []);

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -181,7 +171,6 @@ export const FileSearchCommandDialog = () => {
key={file.path}
file={file}
onSelect={() => onSelect(file)}
onMouseEnter={() => onMouseEnter(file)}
/>
);
})}
Expand All @@ -196,7 +185,6 @@ export const FileSearchCommandDialog = () => {
file={file}
match={match}
onSelect={() => onSelect(file)}
onMouseEnter={() => onMouseEnter(file)}
/>
);
})}
Expand All @@ -223,20 +211,17 @@ interface SearchResultComponentProps {
to: number;
};
onSelect: () => void;
onMouseEnter: () => void;
}

const SearchResultComponent = ({
file,
match,
onSelect,
onMouseEnter,
}: SearchResultComponentProps) => {
return (
<CommandItem
key={file.path}
onSelect={onSelect}
onMouseEnter={onMouseEnter}
>
<div className="flex flex-row gap-2 w-full cursor-pointer relative">
<FileTreeItemIcon item={file} className="mt-1" />
Expand Down
Loading