Skip to content

Commit 7326e63

Browse files
cursoragentmsukkari
andcommitted
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 <msukkari@users.noreply.github.com>
1 parent 9e07fcd commit 7326e63

File tree

3 files changed

+183
-28
lines changed

3 files changed

+183
-28
lines changed

packages/web/src/app/[domain]/browse/hooks/useBrowseNavigation.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,26 @@ export const useBrowseNavigation = () => {
2727
router.push(browsePath);
2828
}, [router]);
2929

30+
const createBrowsePath = useCallback(({
31+
repoName,
32+
revisionName = 'HEAD',
33+
path,
34+
pathType,
35+
highlightRange,
36+
setBrowseState,
37+
}: GetBrowsePathProps) => {
38+
return getBrowsePath({
39+
repoName,
40+
revisionName,
41+
path,
42+
pathType,
43+
highlightRange,
44+
setBrowseState,
45+
});
46+
}, []);
47+
3048
return {
3149
navigateToPath,
50+
createBrowsePath,
3251
};
3352
};

packages/web/src/components/ui/loading-button.tsx

Lines changed: 31 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,28 +2,43 @@
22

33
// @note: this is not a original Shadcn component.
44

5-
import { Button, ButtonProps } from "@/components/ui/button";
5+
import { buttonVariants } from "@/components/ui/button";
6+
import { cn } from "@/lib/utils";
7+
import { Slot } from "@radix-ui/react-slot";
8+
import { VariantProps } from "class-variance-authority";
69
import { Loader2 } from "lucide-react";
710
import React from "react";
811

9-
export interface LoadingButtonProps extends ButtonProps {
12+
export interface LoadingButtonProps
13+
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
14+
VariantProps<typeof buttonVariants> {
1015
loading?: boolean;
16+
asChild?: boolean;
1117
}
1218

13-
const LoadingButton = React.forwardRef<HTMLButtonElement, LoadingButtonProps>(({ children, loading, ...props }, ref) => {
14-
return (
15-
<Button
16-
{...props}
17-
ref={ref}
18-
disabled={loading || props.disabled}
19-
>
20-
{loading && (
21-
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
22-
)}
23-
{children}
24-
</Button>
25-
)
26-
});
19+
const LoadingButton = React.forwardRef<HTMLButtonElement, LoadingButtonProps>(
20+
({ children, loading, className, variant, size, asChild = false, disabled, ...props }, ref) => {
21+
const Comp = asChild ? Slot : "button";
22+
const isDisabled = loading || disabled;
23+
24+
return (
25+
<Comp
26+
className={cn(buttonVariants({ variant, size, className }))}
27+
ref={ref}
28+
disabled={isDisabled}
29+
aria-disabled={isDisabled}
30+
{...props}
31+
>
32+
<>
33+
{loading && (
34+
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
35+
)}
36+
{children}
37+
</>
38+
</Comp>
39+
);
40+
}
41+
);
2742

2843
LoadingButton.displayName = "LoadingButton";
2944

packages/web/src/ee/features/codeNav/components/symbolHoverPopup/index.tsx

Lines changed: 133 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,11 @@ import { Separator } from "@/components/ui/separator";
77
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
88
import { createAuditAction } from "@/ee/features/audit/actions";
99
import useCaptureEvent from "@/hooks/useCaptureEvent";
10+
import { cn } from "@/lib/utils";
1011
import { computePosition, flip, offset, shift, VirtualElement } from "@floating-ui/react";
1112
import { ReactCodeMirrorRef } from "@uiw/react-codemirror";
1213
import { Loader2 } from "lucide-react";
13-
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
14+
import { useCallback, useEffect, useMemo, useRef, useState, MouseEvent } from "react";
1415
import { createPortal } from "react-dom";
1516
import { useHotkeys } from "react-hotkeys-hook";
1617
import { SymbolDefinitionPreview } from "./symbolDefinitionPreview";
@@ -36,7 +37,7 @@ export const SymbolHoverPopup: React.FC<SymbolHoverPopupProps> = ({
3637
const ref = useRef<HTMLDivElement>(null);
3738
const [isSticky, setIsSticky] = useState(false);
3839
const { toast } = useToast();
39-
const { navigateToPath } = useBrowseNavigation();
40+
const { navigateToPath, createBrowsePath } = useBrowseNavigation();
4041
const captureEvent = useCaptureEvent();
4142

4243
const symbolInfo = useHoveredOverSymbolInfo({
@@ -106,6 +107,72 @@ export const SymbolHoverPopup: React.FC<SymbolHoverPopupProps> = ({
106107
return symbolInfo.symbolDefinitions[0];
107108
}, [fileName, repoName, symbolInfo?.symbolDefinitions]);
108109

110+
const gotoDefinitionHref = useMemo(() => {
111+
if (
112+
!symbolInfo ||
113+
!symbolInfo.symbolDefinitions ||
114+
!previewedSymbolDefinition
115+
) {
116+
return undefined;
117+
}
118+
119+
const {
120+
fileName,
121+
repoName,
122+
revisionName,
123+
language,
124+
range: highlightRange,
125+
} = previewedSymbolDefinition;
126+
127+
return createBrowsePath({
128+
repoName,
129+
revisionName,
130+
path: fileName,
131+
pathType: 'blob',
132+
highlightRange,
133+
...(symbolInfo.symbolDefinitions.length > 1 ? {
134+
setBrowseState: {
135+
selectedSymbolInfo: {
136+
symbolName: symbolInfo.symbolName,
137+
repoName,
138+
revisionName,
139+
language,
140+
},
141+
activeExploreMenuTab: "definitions",
142+
isBottomPanelCollapsed: false,
143+
}
144+
} : {}),
145+
});
146+
}, [createBrowsePath, previewedSymbolDefinition, symbolInfo]);
147+
148+
const onGotoDefinitionClick = useCallback((e: MouseEvent<HTMLAnchorElement>) => {
149+
if (
150+
!symbolInfo ||
151+
!symbolInfo.symbolDefinitions ||
152+
!previewedSymbolDefinition
153+
) {
154+
e.preventDefault();
155+
return;
156+
}
157+
158+
captureEvent('wa_goto_definition_pressed', {
159+
source,
160+
});
161+
162+
createAuditAction({
163+
action: "user.performed_goto_definition",
164+
metadata: {
165+
message: symbolInfo.symbolName,
166+
source: 'sourcebot-web-client',
167+
},
168+
});
169+
}, [
170+
captureEvent,
171+
previewedSymbolDefinition,
172+
source,
173+
symbolInfo
174+
]);
175+
109176
const onGotoDefinition = useCallback(() => {
110177
if (
111178
!symbolInfo ||
@@ -136,13 +203,11 @@ export const SymbolHoverPopup: React.FC<SymbolHoverPopupProps> = ({
136203
} = previewedSymbolDefinition;
137204

138205
navigateToPath({
139-
// Always navigate to the preview symbol definition.
140206
repoName,
141207
revisionName,
142208
path: fileName,
143209
pathType: 'blob',
144210
highlightRange,
145-
// If there are multiple definitions, we should open the Explore panel with the definitions.
146211
...(symbolInfo.symbolDefinitions.length > 1 ? {
147212
setBrowseState: {
148213
selectedSymbolInfo: {
@@ -164,6 +229,49 @@ export const SymbolHoverPopup: React.FC<SymbolHoverPopupProps> = ({
164229
symbolInfo
165230
]);
166231

232+
const findReferencesHref = useMemo(() => {
233+
if (!symbolInfo) {
234+
return undefined;
235+
}
236+
237+
return createBrowsePath({
238+
repoName,
239+
revisionName,
240+
path: fileName,
241+
pathType: 'blob',
242+
highlightRange: symbolInfo.range,
243+
setBrowseState: {
244+
selectedSymbolInfo: {
245+
symbolName: symbolInfo.symbolName,
246+
repoName,
247+
revisionName,
248+
language,
249+
},
250+
activeExploreMenuTab: "references",
251+
isBottomPanelCollapsed: false,
252+
}
253+
});
254+
}, [createBrowsePath, fileName, language, repoName, revisionName, symbolInfo]);
255+
256+
const onFindReferencesClick = useCallback((e: MouseEvent<HTMLAnchorElement>) => {
257+
if (!symbolInfo) {
258+
e.preventDefault();
259+
return;
260+
}
261+
262+
captureEvent('wa_find_references_pressed', {
263+
source,
264+
});
265+
266+
createAuditAction({
267+
action: "user.performed_find_references",
268+
metadata: {
269+
message: symbolInfo.symbolName,
270+
source: 'sourcebot-web-client',
271+
},
272+
});
273+
}, [captureEvent, source, symbolInfo]);
274+
167275
const onFindReferences = useCallback(() => {
168276
if (!symbolInfo) {
169277
return;
@@ -276,13 +384,20 @@ export const SymbolHoverPopup: React.FC<SymbolHoverPopupProps> = ({
276384
disabled={!previewedSymbolDefinition}
277385
variant="outline"
278386
size="sm"
279-
onClick={onGotoDefinition}
387+
asChild={!symbolInfo.isSymbolDefinitionsLoading && !!previewedSymbolDefinition}
280388
>
281-
{
282-
!symbolInfo.isSymbolDefinitionsLoading && !previewedSymbolDefinition ?
283-
"No definition found" :
284-
`Go to ${symbolInfo.symbolDefinitions && symbolInfo.symbolDefinitions.length > 1 ? "definitions" : "definition"}`
285-
}
389+
{!symbolInfo.isSymbolDefinitionsLoading && previewedSymbolDefinition ? (
390+
<a
391+
href={gotoDefinitionHref}
392+
onClick={onGotoDefinitionClick}
393+
>
394+
{`Go to ${symbolInfo.symbolDefinitions && symbolInfo.symbolDefinitions.length > 1 ? "definitions" : "definition"}`}
395+
</a>
396+
) : (
397+
<span>
398+
{symbolInfo.isSymbolDefinitionsLoading ? "Loading..." : "No definition found"}
399+
</span>
400+
)}
286401
</LoadingButton>
287402
</TooltipTrigger>
288403
<TooltipContent
@@ -299,9 +414,15 @@ export const SymbolHoverPopup: React.FC<SymbolHoverPopupProps> = ({
299414
<Button
300415
variant="outline"
301416
size="sm"
302-
onClick={onFindReferences}
417+
asChild
303418
>
304-
Find references
419+
<a
420+
href={findReferencesHref}
421+
onClick={onFindReferencesClick}
422+
className={cn(!symbolInfo && "pointer-events-none opacity-50")}
423+
>
424+
Find references
425+
</a>
305426
</Button>
306427
</TooltipTrigger>
307428
<TooltipContent

0 commit comments

Comments
 (0)