mirror of
https://github.com/sourcebot-dev/sourcebot.git
synced 2025-12-12 04:15:30 +00:00
refactor code nav callbacks into symbolHoverPopup
This commit is contained in:
parent
29994d6011
commit
2767c2b088
6 changed files with 157 additions and 270 deletions
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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"}`
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue