refactor code nav callbacks into symbolHoverPopup

This commit is contained in:
bkellam 2025-11-30 15:06:17 -08:00
parent 29994d6011
commit 2767c2b088
6 changed files with 157 additions and 270 deletions

View file

@ -3,7 +3,6 @@
import { ScrollArea } from "@/components/ui/scroll-area"; import { ScrollArea } from "@/components/ui/scroll-area";
import { SymbolHoverPopup } from "@/ee/features/codeNav/components/symbolHoverPopup"; import { SymbolHoverPopup } from "@/ee/features/codeNav/components/symbolHoverPopup";
import { symbolHoverTargetsExtension } from "@/ee/features/codeNav/components/symbolHoverPopup/symbolHoverTargetsExtension"; import { symbolHoverTargetsExtension } from "@/ee/features/codeNav/components/symbolHoverPopup/symbolHoverTargetsExtension";
import { SymbolDefinition } from "@/ee/features/codeNav/components/symbolHoverPopup/useHoveredOverSymbolInfo";
import { useHasEntitlement } from "@/features/entitlements/useHasEntitlement"; import { useHasEntitlement } from "@/features/entitlements/useHasEntitlement";
import { useCodeMirrorLanguageExtension } from "@/hooks/useCodeMirrorLanguageExtension"; import { useCodeMirrorLanguageExtension } from "@/hooks/useCodeMirrorLanguageExtension";
import { useCodeMirrorTheme } from "@/hooks/useCodeMirrorTheme"; import { useCodeMirrorTheme } from "@/hooks/useCodeMirrorTheme";
@ -11,14 +10,10 @@ import { useKeymapExtension } from "@/hooks/useKeymapExtension";
import { useNonEmptyQueryParam } from "@/hooks/useNonEmptyQueryParam"; import { useNonEmptyQueryParam } from "@/hooks/useNonEmptyQueryParam";
import { search } from "@codemirror/search"; import { search } from "@codemirror/search";
import CodeMirror, { EditorSelection, EditorView, ReactCodeMirrorRef, SelectionRange, ViewUpdate } from "@uiw/react-codemirror"; import CodeMirror, { EditorSelection, EditorView, ReactCodeMirrorRef, SelectionRange, ViewUpdate } from "@uiw/react-codemirror";
import { useCallback, useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { EditorContextMenu } from "../../../components/editorContextMenu"; import { EditorContextMenu } from "../../../components/editorContextMenu";
import { useBrowseNavigation } from "../../hooks/useBrowseNavigation";
import { BrowseHighlightRange, HIGHLIGHT_RANGE_QUERY_PARAM } from "../../hooks/utils"; import { BrowseHighlightRange, HIGHLIGHT_RANGE_QUERY_PARAM } from "../../hooks/utils";
import { useBrowseState } from "../../hooks/useBrowseState";
import { rangeHighlightingExtension } from "./rangeHighlightingExtension"; import { rangeHighlightingExtension } from "./rangeHighlightingExtension";
import useCaptureEvent from "@/hooks/useCaptureEvent";
import { createAuditAction } from "@/ee/features/audit/actions";
interface PureCodePreviewPanelProps { interface PureCodePreviewPanelProps {
path: string; path: string;
@ -40,9 +35,6 @@ export const PureCodePreviewPanel = ({
const [currentSelection, setCurrentSelection] = useState<SelectionRange>(); const [currentSelection, setCurrentSelection] = useState<SelectionRange>();
const keymapExtension = useKeymapExtension(editorRef?.view); const keymapExtension = useKeymapExtension(editorRef?.view);
const hasCodeNavEntitlement = useHasEntitlement("code-nav"); const hasCodeNavEntitlement = useHasEntitlement("code-nav");
const { updateBrowseState } = useBrowseState();
const { navigateToPath } = useBrowseNavigation();
const captureEvent = useCaptureEvent();
const highlightRangeQuery = useNonEmptyQueryParam(HIGHLIGHT_RANGE_QUERY_PARAM); const highlightRangeQuery = useNonEmptyQueryParam(HIGHLIGHT_RANGE_QUERY_PARAM);
const highlightRange = useMemo((): BrowseHighlightRange | undefined => { const highlightRange = useMemo((): BrowseHighlightRange | undefined => {
@ -134,72 +126,6 @@ export const PureCodePreviewPanel = ({
}); });
}, [editorRef, highlightRange]); }, [editorRef, highlightRange]);
const onFindReferences = useCallback((symbolName: string) => {
captureEvent('wa_find_references_pressed', {
source: 'browse',
});
createAuditAction({
action: "user.performed_find_references",
metadata: {
message: symbolName,
},
})
updateBrowseState({
selectedSymbolInfo: {
repoName,
symbolName,
revisionName,
language,
},
isBottomPanelCollapsed: false,
activeExploreMenuTab: "references",
})
}, [captureEvent, updateBrowseState, repoName, revisionName, language]);
// If we resolve multiple matches, instead of navigating to the first match, we should
// instead popup the bottom sheet with the list of matches.
const onGotoDefinition = useCallback((symbolName: string, symbolDefinitions: SymbolDefinition[]) => {
captureEvent('wa_goto_definition_pressed', {
source: 'browse',
});
createAuditAction({
action: "user.performed_goto_definition",
metadata: {
message: symbolName,
},
})
if (symbolDefinitions.length === 0) {
return;
}
if (symbolDefinitions.length === 1) {
const symbolDefinition = symbolDefinitions[0];
const { fileName, repoName } = symbolDefinition;
navigateToPath({
repoName,
revisionName,
path: fileName,
pathType: 'blob',
highlightRange: symbolDefinition.range,
})
} else {
updateBrowseState({
selectedSymbolInfo: {
symbolName,
repoName,
revisionName,
language,
},
activeExploreMenuTab: "definitions",
isBottomPanelCollapsed: false,
})
}
}, [captureEvent, navigateToPath, revisionName, updateBrowseState, repoName, language]);
const theme = useCodeMirrorTheme(); const theme = useCodeMirrorTheme();
return ( return (
@ -223,11 +149,12 @@ export const PureCodePreviewPanel = ({
)} )}
{editorRef && hasCodeNavEntitlement && ( {editorRef && hasCodeNavEntitlement && (
<SymbolHoverPopup <SymbolHoverPopup
source="preview"
editorRef={editorRef} editorRef={editorRef}
revisionName={revisionName} revisionName={revisionName}
language={language} language={language}
onFindReferences={onFindReferences} fileName={path}
onGotoDefinition={onGotoDefinition} repoName={repoName}
/> />
)} )}
</CodeMirror> </CodeMirror>

View file

@ -1,12 +1,16 @@
'use client'; 'use client';
import { useBrowseNavigation } from "@/app/[domain]/browse/hooks/useBrowseNavigation";
import { EditorContextMenu } from "@/app/[domain]/components/editorContextMenu"; import { EditorContextMenu } from "@/app/[domain]/components/editorContextMenu";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { ScrollArea } from "@/components/ui/scroll-area"; import { ScrollArea } from "@/components/ui/scroll-area";
import { SymbolHoverPopup } from "@/ee/features/codeNav/components/symbolHoverPopup";
import { symbolHoverTargetsExtension } from "@/ee/features/codeNav/components/symbolHoverPopup/symbolHoverTargetsExtension";
import { useHasEntitlement } from "@/features/entitlements/useHasEntitlement";
import { SearchResultChunk } from "@/features/search"; import { SearchResultChunk } from "@/features/search";
import { useCodeMirrorLanguageExtension } from "@/hooks/useCodeMirrorLanguageExtension";
import { useCodeMirrorTheme } from "@/hooks/useCodeMirrorTheme"; import { useCodeMirrorTheme } from "@/hooks/useCodeMirrorTheme";
import { useKeymapExtension } from "@/hooks/useKeymapExtension"; import { useKeymapExtension } from "@/hooks/useKeymapExtension";
import { useCodeMirrorLanguageExtension } from "@/hooks/useCodeMirrorLanguageExtension";
import { gutterWidthExtension } from "@/lib/extensions/gutterWidthExtension"; import { gutterWidthExtension } from "@/lib/extensions/gutterWidthExtension";
import { highlightRanges, searchResultHighlightExtension } from "@/lib/extensions/searchResultHighlightExtension"; import { highlightRanges, searchResultHighlightExtension } from "@/lib/extensions/searchResultHighlightExtension";
import { search } from "@codemirror/search"; import { search } from "@codemirror/search";
@ -16,13 +20,6 @@ import { Scrollbar } from "@radix-ui/react-scroll-area";
import CodeMirror, { ReactCodeMirrorRef, SelectionRange } from '@uiw/react-codemirror'; import CodeMirror, { ReactCodeMirrorRef, SelectionRange } from '@uiw/react-codemirror';
import { ArrowDown, ArrowUp } from "lucide-react"; import { ArrowDown, ArrowUp } from "lucide-react";
import { Dispatch, SetStateAction, useCallback, useEffect, useMemo, useState } from "react"; import { Dispatch, SetStateAction, useCallback, useEffect, useMemo, useState } from "react";
import { useBrowseNavigation } from "@/app/[domain]/browse/hooks/useBrowseNavigation";
import { SymbolHoverPopup } from "@/ee/features/codeNav/components/symbolHoverPopup";
import { symbolHoverTargetsExtension } from "@/ee/features/codeNav/components/symbolHoverPopup/symbolHoverTargetsExtension";
import { useHasEntitlement } from "@/features/entitlements/useHasEntitlement";
import { SymbolDefinition } from "@/ee/features/codeNav/components/symbolHoverPopup/useHoveredOverSymbolInfo";
import { createAuditAction } from "@/ee/features/audit/actions";
import useCaptureEvent from "@/hooks/useCaptureEvent";
export interface CodePreviewFile { export interface CodePreviewFile {
content: string; content: string;
@ -59,8 +56,6 @@ export const CodePreview = ({
const languageExtension = useCodeMirrorLanguageExtension(file?.language ?? '', editorRef?.view); const languageExtension = useCodeMirrorLanguageExtension(file?.language ?? '', editorRef?.view);
const [currentSelection, setCurrentSelection] = useState<SelectionRange>(); const [currentSelection, setCurrentSelection] = useState<SelectionRange>();
const captureEvent = useCaptureEvent();
const extensions = useMemo(() => { const extensions = useMemo(() => {
return [ return [
keymapExtension, keymapExtension,
@ -115,81 +110,6 @@ export const CodePreview = ({
onSelectedMatchIndexChange((prev) => prev + 1); onSelectedMatchIndexChange((prev) => prev + 1);
}, [onSelectedMatchIndexChange]); }, [onSelectedMatchIndexChange]);
const onGotoDefinition = useCallback((symbolName: string, symbolDefinitions: SymbolDefinition[]) => {
captureEvent('wa_goto_definition_pressed', {
source: 'preview',
});
createAuditAction({
action: "user.performed_goto_definition",
metadata: {
message: symbolName,
},
})
if (symbolDefinitions.length === 0) {
return;
}
if (symbolDefinitions.length === 1) {
const symbolDefinition = symbolDefinitions[0];
const { fileName, repoName } = symbolDefinition;
navigateToPath({
repoName,
revisionName: file.revision,
path: fileName,
pathType: 'blob',
highlightRange: symbolDefinition.range,
})
} else {
navigateToPath({
repoName,
revisionName: file.revision,
path: file.filepath,
pathType: 'blob',
setBrowseState: {
selectedSymbolInfo: {
symbolName,
repoName,
revisionName: file.revision,
language: file.language,
},
activeExploreMenuTab: "definitions",
isBottomPanelCollapsed: false,
}
});
}
}, [captureEvent, file.filepath, file.language, file.revision, navigateToPath, repoName]);
const onFindReferences = useCallback((symbolName: string) => {
captureEvent('wa_find_references_pressed', {
source: 'preview',
});
createAuditAction({
action: "user.performed_find_references",
metadata: {
message: symbolName,
},
})
navigateToPath({
repoName,
revisionName: file.revision,
path: file.filepath,
pathType: 'blob',
setBrowseState: {
selectedSymbolInfo: {
repoName,
symbolName,
revisionName: file.revision,
language: file.language,
},
activeExploreMenuTab: "references",
isBottomPanelCollapsed: false,
}
})
}, [captureEvent, file.filepath, file.language, file.revision, navigateToPath, repoName]);
return ( return (
<div className="flex flex-col h-full"> <div className="flex flex-col h-full">
<div className="flex flex-row bg-accent items-center justify-between pr-3 py-0.5 mt-7"> <div className="flex flex-row bg-accent items-center justify-between pr-3 py-0.5 mt-7">
@ -286,11 +206,12 @@ export const CodePreview = ({
{editorRef && hasCodeNavEntitlement && ( {editorRef && hasCodeNavEntitlement && (
<SymbolHoverPopup <SymbolHoverPopup
source="preview"
editorRef={editorRef} editorRef={editorRef}
language={file.language} language={file.language}
revisionName={file.revision} revisionName={file.revision}
onFindReferences={onFindReferences} fileName={file.filepath}
onGotoDefinition={onGotoDefinition} repoName={repoName}
/> />
)} )}
</CodeMirror> </CodeMirror>

View file

@ -151,7 +151,7 @@ export const ExploreMenu = ({
</span> </span>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent side="top" align="center"> <TooltipContent side="top" align="center">
{isGlobalSearchEnabled ? "Search in current repository only" : "Search all repositories"} Search all repositories
<KeyboardShortcutHint <KeyboardShortcutHint
shortcut="⇧ A" shortcut="⇧ A"
className="ml-2" className="ml-2"

View file

@ -1,42 +1,50 @@
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 { Button } from "@/components/ui/button";
import { LoadingButton } from "@/components/ui/loading-button"; import { LoadingButton } from "@/components/ui/loading-button";
import { Separator } from "@/components/ui/separator"; 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 { computePosition, flip, offset, shift, VirtualElement } from "@floating-ui/react"; import { computePosition, flip, offset, shift, VirtualElement } from "@floating-ui/react";
import { ReactCodeMirrorRef } from "@uiw/react-codemirror"; import { ReactCodeMirrorRef } from "@uiw/react-codemirror";
import { Loader2 } from "lucide-react"; import { Loader2 } from "lucide-react";
import { useCallback, useEffect, useRef, useState } from "react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { SymbolDefinition, useHoveredOverSymbolInfo } from "./useHoveredOverSymbolInfo";
import { SymbolDefinitionPreview } from "./symbolDefinitionPreview";
import { createPortal } from "react-dom"; import { createPortal } from "react-dom";
import { useHotkeys } from "react-hotkeys-hook"; import { useHotkeys } from "react-hotkeys-hook";
import { useToast } from "@/components/hooks/use-toast"; import { SymbolDefinitionPreview } from "./symbolDefinitionPreview";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { useHoveredOverSymbolInfo } from "./useHoveredOverSymbolInfo";
import { KeyboardShortcutHint } from "@/app/components/keyboardShortcutHint";
interface SymbolHoverPopupProps { interface SymbolHoverPopupProps {
editorRef: ReactCodeMirrorRef; editorRef: ReactCodeMirrorRef;
language: string; language: string;
revisionName: string; revisionName: string;
onFindReferences: (symbolName: string) => void; repoName: string;
onGotoDefinition: (symbolName: string, symbolDefinitions: SymbolDefinition[]) => void; fileName: string;
source: 'browse' | 'preview' | 'chat';
} }
export const SymbolHoverPopup: React.FC<SymbolHoverPopupProps> = ({ export const SymbolHoverPopup: React.FC<SymbolHoverPopupProps> = ({
editorRef, editorRef,
revisionName, revisionName,
language, language,
onFindReferences, repoName,
onGotoDefinition: _onGotoDefinition, fileName,
source,
}) => { }) => {
const ref = useRef<HTMLDivElement>(null); const ref = useRef<HTMLDivElement>(null);
const [isSticky, setIsSticky] = useState(false); const [isSticky, setIsSticky] = useState(false);
const { toast } = useToast(); const { toast } = useToast();
const { navigateToPath } = useBrowseNavigation();
const captureEvent = useCaptureEvent();
const symbolInfo = useHoveredOverSymbolInfo({ const symbolInfo = useHoveredOverSymbolInfo({
editorRef, editorRef,
isSticky, isSticky,
revisionName, revisionName,
language, language,
repoName,
}); });
// Positions the popup relative to the symbol // Positions the popup relative to the symbol
@ -77,13 +85,121 @@ export const SymbolHoverPopup: React.FC<SymbolHoverPopupProps> = ({
} }
}, [symbolInfo, editorRef]); }, [symbolInfo, editorRef]);
// Multiple symbol definitions can exist for the same symbol, but we can only navigate
// and display a preview of one. If the symbol definition exists in the current file,
// then we use that one, otherwise we fallback to the first definition in the list.
const previewedSymbolDefinition = useMemo(() => {
if (!symbolInfo?.symbolDefinitions || symbolInfo.symbolDefinitions.length === 0) {
return undefined;
}
const matchingDefinition = symbolInfo.symbolDefinitions.find(
(definition) => (
definition.fileName === fileName && definition.repoName === repoName
)
);
if (matchingDefinition) {
return matchingDefinition;
}
return symbolInfo.symbolDefinitions[0];
}, [fileName, repoName, symbolInfo?.symbolDefinitions]);
const onGotoDefinition = useCallback(() => { const onGotoDefinition = useCallback(() => {
if (!symbolInfo || !symbolInfo.symbolDefinitions) { if (
!symbolInfo ||
!symbolInfo.symbolDefinitions ||
!previewedSymbolDefinition
) {
return; return;
} }
_onGotoDefinition(symbolInfo.symbolName, symbolInfo.symbolDefinitions); captureEvent('wa_goto_definition_pressed', {
}, [symbolInfo, _onGotoDefinition]); source,
});
createAuditAction({
action: "user.performed_goto_definition",
metadata: {
message: symbolInfo.symbolName,
},
});
const {
fileName,
repoName,
revisionName,
language,
range: highlightRange,
} = 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: {
symbolName: symbolInfo.symbolName,
repoName,
revisionName,
language,
},
activeExploreMenuTab: "definitions",
isBottomPanelCollapsed: false,
}
} : {}),
});
}, [
captureEvent,
previewedSymbolDefinition,
navigateToPath,
source,
symbolInfo
]);
const onFindReferences = useCallback((symbolName: string) => {
captureEvent('wa_find_references_pressed', {
source,
});
createAuditAction({
action: "user.performed_find_references",
metadata: {
message: symbolName,
},
})
navigateToPath({
repoName,
revisionName,
path: fileName,
pathType: 'blob',
setBrowseState: {
selectedSymbolInfo: {
symbolName,
repoName,
revisionName,
language,
},
activeExploreMenuTab: "references",
isBottomPanelCollapsed: false,
}
})
}, [
captureEvent,
fileName,
language,
navigateToPath,
repoName,
revisionName,
source
]);
// @todo: We should probably make the behaviour s.t., the ctrl / cmd key needs to be held // @todo: We should probably make the behaviour s.t., the ctrl / cmd key needs to be held
// down to navigate to the definition. We should also only show the underline when the key // down to navigate to the definition. We should also only show the underline when the key
@ -147,9 +263,9 @@ export const SymbolHoverPopup: React.FC<SymbolHoverPopupProps> = ({
<Loader2 className="w-4 h-4 animate-spin" /> <Loader2 className="w-4 h-4 animate-spin" />
Loading... Loading...
</div> </div>
) : symbolInfo.symbolDefinitions && symbolInfo.symbolDefinitions.length > 0 ? ( ) : previewedSymbolDefinition ? (
<SymbolDefinitionPreview <SymbolDefinitionPreview
symbolDefinition={symbolInfo.symbolDefinitions[0]} symbolDefinition={previewedSymbolDefinition}
/> />
) : ( ) : (
<p className="text-sm font-medium text-muted-foreground">No hover info found</p> <p className="text-sm font-medium text-muted-foreground">No hover info found</p>
@ -160,13 +276,13 @@ export const SymbolHoverPopup: React.FC<SymbolHoverPopupProps> = ({
<TooltipTrigger asChild> <TooltipTrigger asChild>
<LoadingButton <LoadingButton
loading={symbolInfo.isSymbolDefinitionsLoading} loading={symbolInfo.isSymbolDefinitionsLoading}
disabled={!symbolInfo.symbolDefinitions || symbolInfo.symbolDefinitions.length === 0} disabled={!previewedSymbolDefinition}
variant="outline" variant="outline"
size="sm" size="sm"
onClick={onGotoDefinition} onClick={onGotoDefinition}
> >
{ {
!symbolInfo.isSymbolDefinitionsLoading && (!symbolInfo.symbolDefinitions || symbolInfo.symbolDefinitions.length === 0) ? !symbolInfo.isSymbolDefinitionsLoading && !previewedSymbolDefinition ?
"No definition found" : "No definition found" :
`Go to ${symbolInfo.symbolDefinitions && symbolInfo.symbolDefinitions.length > 1 ? "definitions" : "definition"}` `Go to ${symbolInfo.symbolDefinitions && symbolInfo.symbolDefinitions.length > 1 ? "definitions" : "definition"}`
} }

View file

@ -12,6 +12,7 @@ interface UseHoveredOverSymbolInfoProps {
isSticky: boolean; isSticky: boolean;
revisionName: string; revisionName: string;
language: string; language: string;
repoName: string;
} }
export type SymbolDefinition = { export type SymbolDefinition = {
@ -19,6 +20,7 @@ export type SymbolDefinition = {
language: string; language: string;
fileName: string; fileName: string;
repoName: string; repoName: string;
revisionName: string;
range: SourceRange; range: SourceRange;
} }
@ -37,6 +39,7 @@ export const useHoveredOverSymbolInfo = ({
isSticky, isSticky,
revisionName, revisionName,
language, language,
repoName,
}: UseHoveredOverSymbolInfoProps): HoveredOverSymbolInfo | undefined => { }: UseHoveredOverSymbolInfoProps): HoveredOverSymbolInfo | undefined => {
const mouseOverTimerRef = useRef<NodeJS.Timeout | null>(null); const mouseOverTimerRef = useRef<NodeJS.Timeout | null>(null);
const mouseOutTimerRef = useRef<NodeJS.Timeout | null>(null); const mouseOutTimerRef = useRef<NodeJS.Timeout | null>(null);
@ -50,12 +53,13 @@ export const useHoveredOverSymbolInfo = ({
}, [symbolElement]); }, [symbolElement]);
const { data: symbolDefinitions, isLoading: isSymbolDefinitionsLoading } = useQuery({ const { data: symbolDefinitions, isLoading: isSymbolDefinitionsLoading } = useQuery({
queryKey: ["definitions", symbolName, revisionName, language, domain], queryKey: ["definitions", symbolName, revisionName, language, domain, repoName],
queryFn: () => unwrapServiceError( queryFn: () => unwrapServiceError(
findSearchBasedSymbolDefinitions({ findSearchBasedSymbolDefinitions({
symbolName: symbolName!, symbolName: symbolName!,
language, language,
revisionName, revisionName,
repoName,
}) })
), ),
select: ((data) => { select: ((data) => {
@ -66,6 +70,7 @@ export const useHoveredOverSymbolInfo = ({
language: file.language, language: file.language,
fileName: file.fileName, fileName: file.fileName,
repoName: file.repository, repoName: file.repository,
revisionName: revisionName,
range: match.range, range: match.range,
} }
}) })

View file

@ -1,10 +1,8 @@
'use client'; 'use client';
import { useBrowseNavigation } from "@/app/[domain]/browse/hooks/useBrowseNavigation";
import { PathHeader } from "@/app/[domain]/components/pathHeader"; import { PathHeader } from "@/app/[domain]/components/pathHeader";
import { SymbolHoverPopup } from '@/ee/features/codeNav/components/symbolHoverPopup'; import { SymbolHoverPopup } from '@/ee/features/codeNav/components/symbolHoverPopup';
import { symbolHoverTargetsExtension } from "@/ee/features/codeNav/components/symbolHoverPopup/symbolHoverTargetsExtension"; import { symbolHoverTargetsExtension } from "@/ee/features/codeNav/components/symbolHoverPopup/symbolHoverTargetsExtension";
import { SymbolDefinition } from '@/ee/features/codeNav/components/symbolHoverPopup/useHoveredOverSymbolInfo';
import { useHasEntitlement } from "@/features/entitlements/useHasEntitlement"; import { useHasEntitlement } from "@/features/entitlements/useHasEntitlement";
import { useCodeMirrorLanguageExtension } from "@/hooks/useCodeMirrorLanguageExtension"; import { useCodeMirrorLanguageExtension } from "@/hooks/useCodeMirrorLanguageExtension";
import { useCodeMirrorTheme } from "@/hooks/useCodeMirrorTheme"; import { useCodeMirrorTheme } from "@/hooks/useCodeMirrorTheme";
@ -12,15 +10,13 @@ import { useKeymapExtension } from "@/hooks/useKeymapExtension";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { Range } from "@codemirror/state"; import { Range } from "@codemirror/state";
import { Decoration, DecorationSet, EditorView } from '@codemirror/view'; import { Decoration, DecorationSet, EditorView } from '@codemirror/view';
import { CodeHostType } from "@sourcebot/db";
import CodeMirror, { ReactCodeMirrorRef, StateField } from '@uiw/react-codemirror'; import CodeMirror, { ReactCodeMirrorRef, StateField } from '@uiw/react-codemirror';
import isEqual from "fast-deep-equal/react";
import { ChevronDown, ChevronRight } from "lucide-react"; import { ChevronDown, ChevronRight } from "lucide-react";
import { forwardRef, memo, Ref, useCallback, useImperativeHandle, useMemo, useState } from "react"; import { forwardRef, memo, Ref, useCallback, useImperativeHandle, useMemo, useState } from "react";
import { FileReference } from "../../types"; import { FileReference } from "../../types";
import { createCodeFoldingExtension } from "./codeFoldingExtension"; import { createCodeFoldingExtension } from "./codeFoldingExtension";
import useCaptureEvent from "@/hooks/useCaptureEvent";
import { CodeHostType } from "@sourcebot/db";
import { createAuditAction } from "@/ee/features/audit/actions";
import isEqual from "fast-deep-equal/react";
const lineDecoration = Decoration.line({ const lineDecoration = Decoration.line({
attributes: { class: "cm-range-border-radius chat-lineHighlight" }, attributes: { class: "cm-range-border-radius chat-lineHighlight" },
@ -74,7 +70,6 @@ const ReferencedFileSourceListItem = ({
}: ReferencedFileSourceListItemProps, forwardedRef: Ref<ReactCodeMirrorRef>) => { }: ReferencedFileSourceListItemProps, forwardedRef: Ref<ReactCodeMirrorRef>) => {
const theme = useCodeMirrorTheme(); const theme = useCodeMirrorTheme();
const [editorRef, setEditorRef] = useState<ReactCodeMirrorRef | null>(null); const [editorRef, setEditorRef] = useState<ReactCodeMirrorRef | null>(null);
const captureEvent = useCaptureEvent();
useImperativeHandle( useImperativeHandle(
forwardedRef, forwardedRef,
@ -84,7 +79,6 @@ const ReferencedFileSourceListItem = ({
const hasCodeNavEntitlement = useHasEntitlement("code-nav"); const hasCodeNavEntitlement = useHasEntitlement("code-nav");
const languageExtension = useCodeMirrorLanguageExtension(language, editorRef?.view); const languageExtension = useCodeMirrorLanguageExtension(language, editorRef?.view);
const { navigateToPath } = useBrowseNavigation();
const getReferenceAtPos = useCallback((x: number, y: number, view: EditorView): FileReference | undefined => { const getReferenceAtPos = useCallback((x: number, y: number, view: EditorView): FileReference | undefined => {
const pos = view.posAtCoords({ x, y }); const pos = view.posAtCoords({ x, y });
@ -217,83 +211,6 @@ const ReferencedFileSourceListItem = ({
codeFoldingExtension, codeFoldingExtension,
]); ]);
const onGotoDefinition = useCallback((symbolName: string, symbolDefinitions: SymbolDefinition[]) => {
if (symbolDefinitions.length === 0) {
return;
}
captureEvent('wa_goto_definition_pressed', {
source: 'chat',
});
createAuditAction({
action: "user.performed_goto_definition",
metadata: {
message: symbolName,
},
});
if (symbolDefinitions.length === 1) {
const symbolDefinition = symbolDefinitions[0];
const { fileName, repoName } = symbolDefinition;
navigateToPath({
repoName,
revisionName: revision,
path: fileName,
pathType: 'blob',
highlightRange: symbolDefinition.range,
})
} else {
navigateToPath({
repoName,
revisionName: revision,
path: fileName,
pathType: 'blob',
setBrowseState: {
selectedSymbolInfo: {
symbolName,
repoName,
revisionName: revision,
language: language,
},
activeExploreMenuTab: "definitions",
isBottomPanelCollapsed: false,
}
});
}
}, [captureEvent, navigateToPath, revision, repoName, fileName, language]);
const onFindReferences = useCallback((symbolName: string) => {
captureEvent('wa_find_references_pressed', {
source: 'chat',
});
createAuditAction({
action: "user.performed_find_references",
metadata: {
message: symbolName,
},
});
navigateToPath({
repoName,
revisionName: revision,
path: fileName,
pathType: 'blob',
setBrowseState: {
selectedSymbolInfo: {
symbolName,
repoName,
revisionName: revision,
language: language,
},
activeExploreMenuTab: "references",
isBottomPanelCollapsed: false,
}
})
}, [captureEvent, fileName, language, navigateToPath, repoName, revision]);
const ExpandCollapseIcon = useMemo(() => { const ExpandCollapseIcon = useMemo(() => {
return isExpanded ? ChevronDown : ChevronRight; return isExpanded ? ChevronDown : ChevronRight;
}, [isExpanded]); }, [isExpanded]);
@ -341,11 +258,12 @@ const ReferencedFileSourceListItem = ({
> >
{editorRef && hasCodeNavEntitlement && ( {editorRef && hasCodeNavEntitlement && (
<SymbolHoverPopup <SymbolHoverPopup
source="chat"
editorRef={editorRef} editorRef={editorRef}
revisionName={revision} revisionName={revision}
language={language} language={language}
onFindReferences={onFindReferences} repoName={repoName}
onGotoDefinition={onGotoDefinition} fileName={fileName}
/> />
)} )}
</CodeMirror> </CodeMirror>