Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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 @@ -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
Expand Down
19 changes: 19 additions & 0 deletions packages/web/src/app/[domain]/browse/hooks/useBrowseNavigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
};
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -36,7 +38,7 @@ export const SymbolHoverPopup: React.FC<SymbolHoverPopupProps> = ({
const ref = useRef<HTMLDivElement>(null);
const [isSticky, setIsSticky] = useState(false);
const { toast } = useToast();
const { navigateToPath } = useBrowseNavigation();
const { navigateToPath, createBrowsePath } = useBrowseNavigation();
const captureEvent = useCaptureEvent();

const symbolInfo = useHoveredOverSymbolInfo({
Expand Down Expand Up @@ -106,6 +108,72 @@ export const SymbolHoverPopup: React.FC<SymbolHoverPopupProps> = ({
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<HTMLAnchorElement>) => {
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 ||
Expand Down Expand Up @@ -136,13 +204,11 @@ export const SymbolHoverPopup: React.FC<SymbolHoverPopupProps> = ({
} = 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: {
Expand All @@ -164,6 +230,49 @@ export const SymbolHoverPopup: React.FC<SymbolHoverPopupProps> = ({
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<HTMLAnchorElement>) => {
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;
Expand Down Expand Up @@ -271,19 +380,24 @@ export const SymbolHoverPopup: React.FC<SymbolHoverPopupProps> = ({
<div className="flex flex-row gap-2 mt-2">
<Tooltip delayDuration={500}>
<TooltipTrigger asChild>
<LoadingButton
loading={symbolInfo.isSymbolDefinitionsLoading}
disabled={!previewedSymbolDefinition}
variant="outline"
size="sm"
onClick={onGotoDefinition}
>
{
!symbolInfo.isSymbolDefinitionsLoading && !previewedSymbolDefinition ?
"No definition found" :
`Go to ${symbolInfo.symbolDefinitions && symbolInfo.symbolDefinitions.length > 1 ? "definitions" : "definition"}`
}
</LoadingButton>
{!symbolInfo.isSymbolDefinitionsLoading && previewedSymbolDefinition && gotoDefinitionHref ? (
<Link
href={gotoDefinitionHref}
onClick={onGotoDefinitionClick}
className={cn(buttonVariants({ variant: "outline", size: "sm" }))}
>
{`Go to ${symbolInfo.symbolDefinitions && symbolInfo.symbolDefinitions.length > 1 ? "definitions" : "definition"}`}
</Link>
) : (
<LoadingButton
loading={symbolInfo.isSymbolDefinitionsLoading}
disabled={!previewedSymbolDefinition}
variant="outline"
size="sm"
>
{symbolInfo.isSymbolDefinitionsLoading ? "Loading..." : "No definition found"}
</LoadingButton>
)}
</TooltipTrigger>
<TooltipContent
side="bottom"
Expand All @@ -296,13 +410,16 @@ export const SymbolHoverPopup: React.FC<SymbolHoverPopupProps> = ({
</Tooltip>
<Tooltip delayDuration={500}>
<TooltipTrigger asChild>
<Button
variant="outline"
size="sm"
onClick={onFindReferences}
<Link
href={findReferencesHref ?? "#"}
onClick={onFindReferencesClick}
className={cn(
buttonVariants({ variant: "outline", size: "sm" }),
!findReferencesHref && "pointer-events-none opacity-50"
)}
>
Find references
</Button>
</Link>
</TooltipTrigger>
<TooltipContent
side="bottom"
Expand Down
Loading