From 7326e63bd405398391b11d3920d3b4a692d1d17a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 2 Apr 2026 16:41:26 +0000 Subject: [PATCH 1/3] feat: allow symbol navigation buttons to open in new tab - Add createBrowsePath to useBrowseNavigation hook to generate URLs - Update LoadingButton component to support asChild prop for anchor elements - Convert symbol hover popup buttons to use anchor elements with href - Users can now cmd+click (ctrl+click) to open definitions/references in new tab Co-authored-by: Michael Sukkarieh --- .../browse/hooks/useBrowseNavigation.ts | 19 +++ .../web/src/components/ui/loading-button.tsx | 47 ++++-- .../components/symbolHoverPopup/index.tsx | 145 ++++++++++++++++-- 3 files changed, 183 insertions(+), 28 deletions(-) diff --git a/packages/web/src/app/[domain]/browse/hooks/useBrowseNavigation.ts b/packages/web/src/app/[domain]/browse/hooks/useBrowseNavigation.ts index e3168bd53..e31931ddc 100644 --- a/packages/web/src/app/[domain]/browse/hooks/useBrowseNavigation.ts +++ b/packages/web/src/app/[domain]/browse/hooks/useBrowseNavigation.ts @@ -27,7 +27,26 @@ export const useBrowseNavigation = () => { router.push(browsePath); }, [router]); + const createBrowsePath = useCallback(({ + repoName, + revisionName = 'HEAD', + path, + pathType, + highlightRange, + setBrowseState, + }: GetBrowsePathProps) => { + return getBrowsePath({ + repoName, + revisionName, + path, + pathType, + highlightRange, + setBrowseState, + }); + }, []); + return { navigateToPath, + createBrowsePath, }; }; \ No newline at end of file diff --git a/packages/web/src/components/ui/loading-button.tsx b/packages/web/src/components/ui/loading-button.tsx index 0639efd96..d80c3a0df 100644 --- a/packages/web/src/components/ui/loading-button.tsx +++ b/packages/web/src/components/ui/loading-button.tsx @@ -2,28 +2,43 @@ // @note: this is not a original Shadcn component. -import { Button, ButtonProps } from "@/components/ui/button"; +import { buttonVariants } from "@/components/ui/button"; +import { cn } from "@/lib/utils"; +import { Slot } from "@radix-ui/react-slot"; +import { VariantProps } from "class-variance-authority"; import { Loader2 } from "lucide-react"; import React from "react"; -export interface LoadingButtonProps extends ButtonProps { +export interface LoadingButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { loading?: boolean; + asChild?: boolean; } -const LoadingButton = React.forwardRef(({ children, loading, ...props }, ref) => { - return ( - - ) -}); +const LoadingButton = React.forwardRef( + ({ children, loading, className, variant, size, asChild = false, disabled, ...props }, ref) => { + const Comp = asChild ? Slot : "button"; + const isDisabled = loading || disabled; + + return ( + + <> + {loading && ( + + )} + {children} + + + ); + } +); LoadingButton.displayName = "LoadingButton"; diff --git a/packages/web/src/ee/features/codeNav/components/symbolHoverPopup/index.tsx b/packages/web/src/ee/features/codeNav/components/symbolHoverPopup/index.tsx index 1e3616f7d..4373925ee 100644 --- a/packages/web/src/ee/features/codeNav/components/symbolHoverPopup/index.tsx +++ b/packages/web/src/ee/features/codeNav/components/symbolHoverPopup/index.tsx @@ -7,10 +7,11 @@ import { Separator } from "@/components/ui/separator"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { createAuditAction } from "@/ee/features/audit/actions"; import useCaptureEvent from "@/hooks/useCaptureEvent"; +import { cn } from "@/lib/utils"; import { computePosition, flip, offset, shift, VirtualElement } from "@floating-ui/react"; import { ReactCodeMirrorRef } from "@uiw/react-codemirror"; import { Loader2 } from "lucide-react"; -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState, MouseEvent } from "react"; import { createPortal } from "react-dom"; import { useHotkeys } from "react-hotkeys-hook"; import { SymbolDefinitionPreview } from "./symbolDefinitionPreview"; @@ -36,7 +37,7 @@ export const SymbolHoverPopup: React.FC = ({ const ref = useRef(null); const [isSticky, setIsSticky] = useState(false); const { toast } = useToast(); - const { navigateToPath } = useBrowseNavigation(); + const { navigateToPath, createBrowsePath } = useBrowseNavigation(); const captureEvent = useCaptureEvent(); const symbolInfo = useHoveredOverSymbolInfo({ @@ -106,6 +107,72 @@ export const SymbolHoverPopup: React.FC = ({ return symbolInfo.symbolDefinitions[0]; }, [fileName, repoName, symbolInfo?.symbolDefinitions]); + const gotoDefinitionHref = useMemo(() => { + if ( + !symbolInfo || + !symbolInfo.symbolDefinitions || + !previewedSymbolDefinition + ) { + return undefined; + } + + const { + fileName, + repoName, + revisionName, + language, + range: highlightRange, + } = previewedSymbolDefinition; + + return createBrowsePath({ + repoName, + revisionName, + path: fileName, + pathType: 'blob', + highlightRange, + ...(symbolInfo.symbolDefinitions.length > 1 ? { + setBrowseState: { + selectedSymbolInfo: { + symbolName: symbolInfo.symbolName, + repoName, + revisionName, + language, + }, + activeExploreMenuTab: "definitions", + isBottomPanelCollapsed: false, + } + } : {}), + }); + }, [createBrowsePath, previewedSymbolDefinition, symbolInfo]); + + const onGotoDefinitionClick = useCallback((e: MouseEvent) => { + if ( + !symbolInfo || + !symbolInfo.symbolDefinitions || + !previewedSymbolDefinition + ) { + e.preventDefault(); + return; + } + + captureEvent('wa_goto_definition_pressed', { + source, + }); + + createAuditAction({ + action: "user.performed_goto_definition", + metadata: { + message: symbolInfo.symbolName, + source: 'sourcebot-web-client', + }, + }); + }, [ + captureEvent, + previewedSymbolDefinition, + source, + symbolInfo + ]); + const onGotoDefinition = useCallback(() => { if ( !symbolInfo || @@ -136,13 +203,11 @@ export const SymbolHoverPopup: React.FC = ({ } = previewedSymbolDefinition; navigateToPath({ - // Always navigate to the preview symbol definition. repoName, revisionName, path: fileName, pathType: 'blob', highlightRange, - // If there are multiple definitions, we should open the Explore panel with the definitions. ...(symbolInfo.symbolDefinitions.length > 1 ? { setBrowseState: { selectedSymbolInfo: { @@ -164,6 +229,49 @@ export const SymbolHoverPopup: React.FC = ({ symbolInfo ]); + const findReferencesHref = useMemo(() => { + if (!symbolInfo) { + return undefined; + } + + return createBrowsePath({ + repoName, + revisionName, + path: fileName, + pathType: 'blob', + highlightRange: symbolInfo.range, + setBrowseState: { + selectedSymbolInfo: { + symbolName: symbolInfo.symbolName, + repoName, + revisionName, + language, + }, + activeExploreMenuTab: "references", + isBottomPanelCollapsed: false, + } + }); + }, [createBrowsePath, fileName, language, repoName, revisionName, symbolInfo]); + + const onFindReferencesClick = useCallback((e: MouseEvent) => { + if (!symbolInfo) { + e.preventDefault(); + return; + } + + captureEvent('wa_find_references_pressed', { + source, + }); + + createAuditAction({ + action: "user.performed_find_references", + metadata: { + message: symbolInfo.symbolName, + source: 'sourcebot-web-client', + }, + }); + }, [captureEvent, source, symbolInfo]); + const onFindReferences = useCallback(() => { if (!symbolInfo) { return; @@ -276,13 +384,20 @@ export const SymbolHoverPopup: React.FC = ({ disabled={!previewedSymbolDefinition} variant="outline" size="sm" - onClick={onGotoDefinition} + asChild={!symbolInfo.isSymbolDefinitionsLoading && !!previewedSymbolDefinition} > - { - !symbolInfo.isSymbolDefinitionsLoading && !previewedSymbolDefinition ? - "No definition found" : - `Go to ${symbolInfo.symbolDefinitions && symbolInfo.symbolDefinitions.length > 1 ? "definitions" : "definition"}` - } + {!symbolInfo.isSymbolDefinitionsLoading && previewedSymbolDefinition ? ( + + {`Go to ${symbolInfo.symbolDefinitions && symbolInfo.symbolDefinitions.length > 1 ? "definitions" : "definition"}`} + + ) : ( + + {symbolInfo.isSymbolDefinitionsLoading ? "Loading..." : "No definition found"} + + )} = ({ Date: Thu, 2 Apr 2026 17:27:24 +0000 Subject: [PATCH 2/3] docs: add changelog entry for symbol navigation new tab feature Co-authored-by: Michael Sukkarieh --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 424544c3d..a0d7e4d8b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added +- Added support for opening code navigation buttons ("Go to definition" and "Find references") in a new tab via Cmd+click / Ctrl+click. [#1079](https://github.com/sourcebot-dev/sourcebot/pull/1079) + ## [4.16.5] - 2026-04-02 ### Added From 09f4cb34dec33ee28c70a20b72ff78847f9709e7 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 2 Apr 2026 17:43:35 +0000 Subject: [PATCH 3/3] fix: use Next.js Link for symbol nav buttons to support Cmd+click on Mac - Replace anchor elements with Next.js Link components for proper Cmd+click (Mac) / Ctrl+click (Windows) support to open in new tabs - Revert LoadingButton asChild changes since we now use Link directly - Buttons now properly support native browser behavior for opening links Co-authored-by: Michael Sukkarieh --- .../web/src/components/ui/loading-button.tsx | 47 +++++--------- .../components/symbolHoverPopup/index.tsx | 62 +++++++++---------- 2 files changed, 45 insertions(+), 64 deletions(-) diff --git a/packages/web/src/components/ui/loading-button.tsx b/packages/web/src/components/ui/loading-button.tsx index d80c3a0df..0639efd96 100644 --- a/packages/web/src/components/ui/loading-button.tsx +++ b/packages/web/src/components/ui/loading-button.tsx @@ -2,43 +2,28 @@ // @note: this is not a original Shadcn component. -import { buttonVariants } from "@/components/ui/button"; -import { cn } from "@/lib/utils"; -import { Slot } from "@radix-ui/react-slot"; -import { VariantProps } from "class-variance-authority"; +import { Button, ButtonProps } from "@/components/ui/button"; import { Loader2 } from "lucide-react"; import React from "react"; -export interface LoadingButtonProps - extends React.ButtonHTMLAttributes, - VariantProps { +export interface LoadingButtonProps extends ButtonProps { loading?: boolean; - asChild?: boolean; } -const LoadingButton = React.forwardRef( - ({ children, loading, className, variant, size, asChild = false, disabled, ...props }, ref) => { - const Comp = asChild ? Slot : "button"; - const isDisabled = loading || disabled; - - return ( - - <> - {loading && ( - - )} - {children} - - - ); - } -); +const LoadingButton = React.forwardRef(({ children, loading, ...props }, ref) => { + return ( + + ) +}); LoadingButton.displayName = "LoadingButton"; diff --git a/packages/web/src/ee/features/codeNav/components/symbolHoverPopup/index.tsx b/packages/web/src/ee/features/codeNav/components/symbolHoverPopup/index.tsx index 4373925ee..632734791 100644 --- a/packages/web/src/ee/features/codeNav/components/symbolHoverPopup/index.tsx +++ b/packages/web/src/ee/features/codeNav/components/symbolHoverPopup/index.tsx @@ -1,7 +1,7 @@ import { useBrowseNavigation } from "@/app/[domain]/browse/hooks/useBrowseNavigation"; import { KeyboardShortcutHint } from "@/app/components/keyboardShortcutHint"; import { useToast } from "@/components/hooks/use-toast"; -import { Button } from "@/components/ui/button"; +import { buttonVariants } from "@/components/ui/button"; import { LoadingButton } from "@/components/ui/loading-button"; import { Separator } from "@/components/ui/separator"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; @@ -11,6 +11,7 @@ import { cn } from "@/lib/utils"; import { computePosition, flip, offset, shift, VirtualElement } from "@floating-ui/react"; import { ReactCodeMirrorRef } from "@uiw/react-codemirror"; import { Loader2 } from "lucide-react"; +import Link from "next/link"; import { useCallback, useEffect, useMemo, useRef, useState, MouseEvent } from "react"; import { createPortal } from "react-dom"; import { useHotkeys } from "react-hotkeys-hook"; @@ -379,26 +380,24 @@ export const SymbolHoverPopup: React.FC = ({
- - {!symbolInfo.isSymbolDefinitionsLoading && previewedSymbolDefinition ? ( - - {`Go to ${symbolInfo.symbolDefinitions && symbolInfo.symbolDefinitions.length > 1 ? "definitions" : "definition"}`} - - ) : ( - - {symbolInfo.isSymbolDefinitionsLoading ? "Loading..." : "No definition found"} - - )} - + {!symbolInfo.isSymbolDefinitionsLoading && previewedSymbolDefinition && gotoDefinitionHref ? ( + + {`Go to ${symbolInfo.symbolDefinitions && symbolInfo.symbolDefinitions.length > 1 ? "definitions" : "definition"}`} + + ) : ( + + {symbolInfo.isSymbolDefinitionsLoading ? "Loading..." : "No definition found"} + + )} = ({ - + Find references +