diff --git a/CHANGELOG.md b/CHANGELOG.md index ddf0a10c1..b3048ede1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ 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) - Linear issue links in chat responses now render as a rich card-style UI showing the Linear logo, issue identifier, and title instead of plain hyperlinks. [#1060](https://github.com/sourcebot-dev/sourcebot/pull/1060) ### Changed 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/ee/features/codeNav/components/symbolHoverPopup/index.tsx b/packages/web/src/ee/features/codeNav/components/symbolHoverPopup/index.tsx index 1e3616f7d..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,16 +1,18 @@ 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"; 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 Link from "next/link"; +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 +38,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 +108,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 +204,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 +230,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; @@ -271,19 +380,24 @@ export const SymbolHoverPopup: React.FC = ({
- - { - !symbolInfo.isSymbolDefinitionsLoading && !previewedSymbolDefinition ? - "No definition found" : - `Go to ${symbolInfo.symbolDefinitions && symbolInfo.symbolDefinitions.length > 1 ? "definitions" : "definition"}` - } - + {!symbolInfo.isSymbolDefinitionsLoading && previewedSymbolDefinition && gotoDefinitionHref ? ( + + {`Go to ${symbolInfo.symbolDefinitions && symbolInfo.symbolDefinitions.length > 1 ? "definitions" : "definition"}`} + + ) : ( + + {symbolInfo.isSymbolDefinitionsLoading ? "Loading..." : "No definition found"} + + )} = ({ - +