diff --git a/CHANGELOG.md b/CHANGELOG.md index 81353ddeb..2616e1e65 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Improved repository query performance by adding db indices. [#526](https://github.com/sourcebot-dev/sourcebot/pull/526) - Improved repository query performance by removing JOIN on `Connection` table. [#527](https://github.com/sourcebot-dev/sourcebot/pull/527) - Changed repo carousel and repo list links to redirect to the file browser. [#528](https://github.com/sourcebot-dev/sourcebot/pull/528) +- Changed file headers, files/directories in file tree, and reference list buttons into links. [#532](https://github.com/sourcebot-dev/sourcebot/pull/532) ## [4.7.1] - 2025-09-19 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 178601859..cdb8f3ca7 100644 --- a/packages/web/src/app/[domain]/browse/[...path]/components/pureTreePreviewPanel.tsx +++ b/packages/web/src/app/[domain]/browse/[...path]/components/pureTreePreviewPanel.tsx @@ -1,11 +1,12 @@ 'use client'; -import { useCallback, useRef } from "react"; +import { useRef } from "react"; import { FileTreeItem } from "@/features/fileTree/actions"; import { FileTreeItemComponent } from "@/features/fileTree/components/fileTreeItemComponent"; -import { useBrowseNavigation } from "../../hooks/useBrowseNavigation"; +import { getBrowsePath } from "../../hooks/useBrowseNavigation"; import { ScrollArea } from "@/components/ui/scroll-area"; import { useBrowseParams } from "../../hooks/useBrowseParams"; +import { useDomain } from "@/hooks/useDomain"; interface PureTreePreviewPanelProps { items: FileTreeItem[]; @@ -13,18 +14,9 @@ interface PureTreePreviewPanelProps { export const PureTreePreviewPanel = ({ items }: PureTreePreviewPanelProps) => { const { repoName, revisionName } = useBrowseParams(); - const { navigateToPath } = useBrowseNavigation(); 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 domain = useDomain(); + return ( { isActive={false} depth={0} isCollapseChevronVisible={false} - onClick={() => onNodeClicked(item)} parentRef={scrollAreaRef} + href={getBrowsePath({ + repoName, + revisionName, + path: item.path, + pathType: item.type === 'tree' ? 'tree' : 'blob', + domain, + })} /> ))} diff --git a/packages/web/src/app/[domain]/components/pathHeader.tsx b/packages/web/src/app/[domain]/components/pathHeader.tsx index bdde8a162..188069370 100644 --- a/packages/web/src/app/[domain]/components/pathHeader.tsx +++ b/packages/web/src/app/[domain]/components/pathHeader.tsx @@ -3,7 +3,7 @@ import { cn, getCodeHostInfoForRepo } from "@/lib/utils"; import { LaptopIcon } from "@radix-ui/react-icons"; import Image from "next/image"; -import { useBrowseNavigation } from "../browse/hooks/useBrowseNavigation"; +import { getBrowsePath } from "../browse/hooks/useBrowseNavigation"; import { ChevronRight, MoreHorizontal } from "lucide-react"; import { useCallback, useState, useMemo, useRef, useEffect } from "react"; import { useToast } from "@/components/hooks/use-toast"; @@ -15,6 +15,8 @@ import { } from "@/components/ui/dropdown-menu"; import { VscodeFileIcon } from "@/app/components/vscodeFileIcon"; import { CopyIconButton } from "./copyIconButton"; +import Link from "next/link"; +import { useDomain } from "@/hooks/useDomain"; interface FileHeaderProps { path: string; @@ -64,11 +66,11 @@ export const PathHeader = ({ webUrl: repo.webUrl, }); - const { navigateToPath } = useBrowseNavigation(); const { toast } = useToast(); const containerRef = useRef(null); const breadcrumbsRef = useRef(null); const [visibleSegmentCount, setVisibleSegmentCount] = useState(null); + const domain = useDomain(); // Create breadcrumb segments from file path const breadcrumbSegments = useMemo(() => { @@ -179,16 +181,6 @@ export const PathHeader = ({ return true; }, [path, toast]); - const onBreadcrumbClick = useCallback((segment: BreadcrumbSegment) => { - navigateToPath({ - repoName: repo.name, - path: segment.fullPath, - pathType: segment.isLastSegment ? pathType : 'tree', - revisionName: branchDisplayName, - }); - }, [repo.name, branchDisplayName, navigateToPath, pathType]); - - const renderSegmentWithHighlight = (segment: BreadcrumbSegment) => { if (!segment.highlightRange) { return segment.name; @@ -224,17 +216,18 @@ export const PathHeader = ({ )} -
navigateToPath({ + href={getBrowsePath({ repoName: repo.name, - path: '', + path: '/', pathType: 'tree', revisionName: branchDisplayName, + domain, })} > {info?.displayName} -
+ {branchDisplayName && (

{hiddenSegments.map((segment) => ( - onBreadcrumbClick(segment)} - className="font-mono text-sm cursor-pointer" > - {renderSegmentWithHighlight(segment)} - + + {renderSegmentWithHighlight(segment)} + + ))} @@ -281,14 +282,20 @@ export const PathHeader = ({ {(isFileIconVisible && index === visibleSegments.length - 1) && ( )} - onBreadcrumbClick(segment)} + href={getBrowsePath({ + repoName: repo.name, + path: segment.fullPath, + pathType: segment.isLastSegment ? pathType : 'tree', + revisionName: branchDisplayName, + domain, + })} > {renderSegmentWithHighlight(segment)} - + {index < visibleSegments.length - 1 && ( )} diff --git a/packages/web/src/app/[domain]/search/components/searchResultsPanel/fileMatch.tsx b/packages/web/src/app/[domain]/search/components/searchResultsPanel/fileMatch.tsx index a24b8e45e..3b1943ba1 100644 --- a/packages/web/src/app/[domain]/search/components/searchResultsPanel/fileMatch.tsx +++ b/packages/web/src/app/[domain]/search/components/searchResultsPanel/fileMatch.tsx @@ -1,27 +1,22 @@ 'use client'; -import { useCallback } from "react"; import { SearchResultFile, SearchResultChunk } from "@/features/search/types"; import { LightweightCodeHighlighter } from "@/app/[domain]/components/lightweightCodeHighlighter"; +import Link from "next/link"; +import { getBrowsePath } from "@/app/[domain]/browse/hooks/useBrowseNavigation"; +import { useDomain } from "@/hooks/useDomain"; interface FileMatchProps { match: SearchResultChunk; file: SearchResultFile; - onOpen: (startLineNumber: number, endLineNumber: number, isCtrlKeyPressed: boolean) => void; } export const FileMatch = ({ match, file, - onOpen: _onOpen, }: FileMatchProps) => { - const onOpen = useCallback((isCtrlKeyPressed: boolean) => { - const startLineNumber = match.contentStart.lineNumber; - const endLineNumber = match.content.trimEnd().split('\n').length + startLineNumber - 1; - - _onOpen(startLineNumber, endLineNumber, isCtrlKeyPressed); - }, [match.content, match.contentStart.lineNumber, _onOpen]); + const domain = useDomain(); // If it's just the title, don't show a code preview if (match.matchRanges.length === 0) { @@ -29,19 +24,24 @@ export const FileMatch = ({ } return ( -

{ - if (e.key !== "Enter") { - return; + href={getBrowsePath({ + repoName: file.repository, + revisionName: file.branches?.[0] ?? 'HEAD', + path: file.fileName.text, + pathType: 'blob', + domain, + highlightRange: { + start: { + lineNumber: match.contentStart.lineNumber, + }, + end: { + lineNumber: match.content.trimEnd().split('\n').length + match.contentStart.lineNumber - 1, + } } - - onOpen(e.metaKey || e.ctrlKey); - }} - onClick={(e) => { - onOpen(e.metaKey || e.ctrlKey); - }} + })} title="open file: click, open file preview: cmd/ctrl + click" > {match.content} -
+ ); } \ No newline at end of file diff --git a/packages/web/src/app/[domain]/search/components/searchResultsPanel/fileMatchContainer.tsx b/packages/web/src/app/[domain]/search/components/searchResultsPanel/fileMatchContainer.tsx index 820521b9f..b10d656a3 100644 --- a/packages/web/src/app/[domain]/search/components/searchResultsPanel/fileMatchContainer.tsx +++ b/packages/web/src/app/[domain]/search/components/searchResultsPanel/fileMatchContainer.tsx @@ -7,7 +7,6 @@ import { useMemo } from "react"; import { FileMatch } from "./fileMatch"; import { RepositoryInfo, SearchResultFile } from "@/features/search/types"; import { Button } from "@/components/ui/button"; -import { useBrowseNavigation } from "@/app/[domain]/browse/hooks/useBrowseNavigation"; export const MAX_MATCHES_TO_PREVIEW = 3; @@ -33,7 +32,6 @@ export const FileMatchContainer = ({ const matchCount = useMemo(() => { return file.chunks.length; }, [file]); - const { navigateToPath } = useBrowseNavigation(); const matches = useMemo(() => { const sortedMatches = file.chunks.sort((a, b) => { @@ -123,29 +121,6 @@ export const FileMatchContainer = ({ { - if (isCtrlKeyPressed) { - const matchIndex = matches.slice(0, index).reduce((acc, match) => { - return acc + match.matchRanges.length; - }, 0); - onOpenFilePreview(matchIndex); - } else { - navigateToPath({ - repoName: file.repository, - revisionName: file.branches?.[0] ?? 'HEAD', - path: file.fileName.text, - pathType: 'blob', - highlightRange: { - start: { - lineNumber: startLineNumber, - }, - end: { - lineNumber: endLineNumber, - } - } - }); - } - }} /> {(index !== matches.length - 1 || isMoreContentButtonVisible) && ( 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 0458ecbc6..831bebcdb 100644 --- a/packages/web/src/ee/features/codeNav/components/exploreMenu/referenceList.tsx +++ b/packages/web/src/ee/features/codeNav/components/exploreMenu/referenceList.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useBrowseNavigation } from "@/app/[domain]/browse/hooks/useBrowseNavigation"; +import { getBrowsePath } from "@/app/[domain]/browse/hooks/useBrowseNavigation"; import { PathHeader } from "@/app/[domain]/components/pathHeader"; import { LightweightCodeHighlighter } from "@/app/[domain]/components/lightweightCodeHighlighter"; import { FindRelatedSymbolsResponse } from "@/features/codeNav/types"; @@ -8,6 +8,8 @@ import { RepositoryInfo, SourceRange } from "@/features/search/types"; import { useMemo, useRef } from "react"; import useCaptureEvent from "@/hooks/useCaptureEvent"; import { useVirtualizer } from "@tanstack/react-virtual"; +import Link from "next/link"; +import { useDomain } from "@/hooks/useDomain"; interface ReferenceListProps { data: FindRelatedSymbolsResponse; @@ -21,6 +23,7 @@ export const ReferenceList = ({ data, revisionName, }: ReferenceListProps) => { + const domain = useDomain(); const repoInfoMap = useMemo(() => { return data.repositoryInfo.reduce((acc, repo) => { acc[repo.id] = repo; @@ -28,7 +31,6 @@ export const ReferenceList = ({ }, {} as Record); }, [data.repositoryInfo]); - const { navigateToPath } = useBrowseNavigation(); const captureEvent = useCaptureEvent(); // Virtualization setup @@ -38,7 +40,7 @@ export const ReferenceList = ({ getScrollElement: () => parentRef.current, estimateSize: (index) => { const file = data.files[index]; - + const estimatedSize = file.matches.length * ESTIMATED_LINE_HEIGHT_PX + ESTIMATED_MATCH_CONTAINER_HEIGHT_PX; @@ -103,22 +105,26 @@ export const ReferenceList = ({ {file.matches .sort((a, b) => a.range.start.lineNumber - b.range.start.lineNumber) .map((match, index) => ( - { captureEvent('wa_explore_menu_reference_clicked', {}); - navigateToPath({ - repoName: file.repository, - revisionName, - path: file.fileName, - pathType: 'blob', - highlightRange: match.range, - }) }} - /> + key={index} + > + + ))} @@ -134,21 +140,18 @@ interface ReferenceListItemProps { lineContent: string; range: SourceRange; language: string; - onClick: () => void; } const ReferenceListItem = ({ lineContent, range, language, - onClick, }: ReferenceListItemProps) => { const highlightRanges = useMemo(() => [range], [range]); return (
void, + href: string, + onClick?: (e: React.MouseEvent) => void, + onNavigate?: (e: { preventDefault: () => void }) => void, parentRef: React.RefObject, }) => { - const ref = useRef(null); + const ref = useRef(null); useEffect(() => { if (isActive && ref.current) { @@ -51,20 +56,16 @@ export const FileTreeItemComponent = ({ }, [isActive, parentRef]); return ( -
{ - if (e.key === 'Enter') { - e.preventDefault(); - onClick(); - } - }} onClick={onClick} + onNavigate={onNavigate} >
{node.name} -
+ ) } diff --git a/packages/web/src/features/fileTree/components/pureFileTreePanel.tsx b/packages/web/src/features/fileTree/components/pureFileTreePanel.tsx index bc7347fb7..88c1ab2f1 100644 --- a/packages/web/src/features/fileTree/components/pureFileTreePanel.tsx +++ b/packages/web/src/features/fileTree/components/pureFileTreePanel.tsx @@ -4,9 +4,9 @@ import { FileTreeNode as RawFileTreeNode } from "../actions"; import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area"; import React, { useCallback, useMemo, useState, useEffect, useRef } from "react"; import { FileTreeItemComponent } from "./fileTreeItemComponent"; -import { useBrowseNavigation } from "@/app/[domain]/browse/hooks/useBrowseNavigation"; +import { getBrowsePath } from "@/app/[domain]/browse/hooks/useBrowseNavigation"; import { useBrowseParams } from "@/app/[domain]/browse/hooks/useBrowseParams"; - +import { useDomain } from "@/hooks/useDomain"; export type FileTreeNode = Omit & { isCollapsed: boolean; @@ -41,8 +41,8 @@ interface PureFileTreePanelProps { export const PureFileTreePanel = ({ tree: _tree, path }: PureFileTreePanelProps) => { const [tree, setTree] = useState(buildCollapsibleTree(_tree)); const scrollAreaRef = useRef(null); - const { navigateToPath } = useBrowseNavigation(); const { repoName, revisionName } = useBrowseParams(); + const domain = useDomain(); // @note: When `_tree` changes, it indicates that a new tree has been loaded. // In that case, we need to rebuild the collapsible tree. @@ -72,21 +72,6 @@ export const PureFileTreePanel = ({ tree: _tree, path }: PureFileTreePanelProps) } }, [path, setIsCollapsed]); - const onNodeClicked = useCallback((node: FileTreeNode) => { - if (node.type === 'tree') { - setIsCollapsed(node.path, !node.isCollapsed); - } - else if (node.type === 'blob') { - navigateToPath({ - repoName: repoName, - revisionName: revisionName, - path: node.path, - pathType: 'blob', - }); - - } - }, [setIsCollapsed, navigateToPath, repoName, revisionName]); - const renderTree = useCallback((nodes: FileTreeNode, depth = 0): React.ReactNode => { return ( <> @@ -94,13 +79,35 @@ export const PureFileTreePanel = ({ tree: _tree, path }: PureFileTreePanelProps) return ( onNodeClicked(node)} + // Only collapse the tree when a regular click happens. + // (i.e., not ctrl/cmd click). + onClick={(e) => { + const isMetaOrCtrlKey = e.metaKey || e.ctrlKey; + if (node.type === 'tree' && !isMetaOrCtrlKey) { + setIsCollapsed(node.path, !node.isCollapsed); + } + }} + // @note: onNavigate _won't_ be called when the user ctrl/cmd clicks on a tree node. + // So when a regular click happens, we want to prevent the navigation from happening + // and instead collapse the tree. + onNavigate={(e) => { + if (node.type === 'tree') { + e.preventDefault(); + } + }} parentRef={scrollAreaRef} /> {node.children.length > 0 && !node.isCollapsed && renderTree(node, depth + 1)} @@ -109,7 +116,7 @@ export const PureFileTreePanel = ({ tree: _tree, path }: PureFileTreePanelProps) })} ); - }, [path, onNodeClicked]); + }, [path]); const renderedTree = useMemo(() => renderTree(tree), [tree, renderTree]);