'use client'; import { EditorContextMenu } from "@/app/[domain]/components/editorContextMenu"; import { Button } from "@/components/ui/button"; import { ScrollArea } from "@/components/ui/scroll-area"; import { SearchResultChunk } from "@/features/search/types"; import { useCodeMirrorTheme } from "@/hooks/useCodeMirrorTheme"; import { useKeymapExtension } from "@/hooks/useKeymapExtension"; import { useCodeMirrorLanguageExtension } from "@/hooks/useCodeMirrorLanguageExtension"; import { gutterWidthExtension } from "@/lib/extensions/gutterWidthExtension"; import { highlightRanges, searchResultHighlightExtension } from "@/lib/extensions/searchResultHighlightExtension"; import { search } from "@codemirror/search"; import { EditorView } from "@codemirror/view"; import { Cross1Icon, FileIcon } from "@radix-ui/react-icons"; import { Scrollbar } from "@radix-ui/react-scroll-area"; import CodeMirror, { ReactCodeMirrorRef, SelectionRange } from '@uiw/react-codemirror'; import { ArrowDown, ArrowUp } from "lucide-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 useCaptureEvent from "@/hooks/useCaptureEvent"; export interface CodePreviewFile { content: string; filepath: string; link?: string; matches: SearchResultChunk[]; language: string; revision: string; } interface CodePreviewProps { file: CodePreviewFile; repoName: string; selectedMatchIndex: number; onSelectedMatchIndexChange: Dispatch>; onClose: () => void; } export const CodePreview = ({ file, repoName, selectedMatchIndex, onSelectedMatchIndexChange, onClose, }: CodePreviewProps) => { const [editorRef, setEditorRef] = useState(null); const { navigateToPath } = useBrowseNavigation(); const hasCodeNavEntitlement = useHasEntitlement("code-nav"); const [gutterWidth, setGutterWidth] = useState(0); const theme = useCodeMirrorTheme(); const keymapExtension = useKeymapExtension(editorRef?.view); const languageExtension = useCodeMirrorLanguageExtension(file?.language ?? '', editorRef?.view); const [currentSelection, setCurrentSelection] = useState(); const captureEvent = useCaptureEvent(); const extensions = useMemo(() => { return [ keymapExtension, gutterWidthExtension, languageExtension, EditorView.lineWrapping, searchResultHighlightExtension(), search({ top: true, }), EditorView.updateListener.of((update) => { const width = update.view.plugin(gutterWidthExtension)?.width; if (width) { setGutterWidth(width); } }), EditorView.updateListener.of((update) => { // @note: it's important we reset the selection when // the document changes... otherwise we will get a floating // context menu where it shouldn't be. if (update.selectionSet || update.docChanged) { setCurrentSelection(update.state.selection.main); } }), hasCodeNavEntitlement ? symbolHoverTargetsExtension : [], ]; }, [hasCodeNavEntitlement, keymapExtension, languageExtension]); const ranges = useMemo(() => { if (!file.matches.length) { return []; } return file.matches.flatMap((match) => { return match.matchRanges; }) }, [file]); useEffect(() => { if (!editorRef?.view) { return; } highlightRanges(selectedMatchIndex, ranges, editorRef.view); }, [ranges, selectedMatchIndex, file, editorRef]); const onUpClicked = useCallback(() => { onSelectedMatchIndexChange((prev) => prev - 1); }, [onSelectedMatchIndexChange]); const onDownClicked = useCallback(() => { onSelectedMatchIndexChange((prev) => prev + 1); }, [onSelectedMatchIndexChange]); const onGotoDefinition = useCallback((symbolName: string, symbolDefinitions: SymbolDefinition[]) => { captureEvent('wa_preview_panel_goto_definition_pressed', {}); 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_preview_panel_find_references_pressed', {}); 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 (
{/* Gutter icon */}
{/* File path */}
{ navigateToPath({ repoName, path: file.filepath, pathType: 'blob', revisionName: file.revision, }); }} title={file.filepath} > {file.filepath}
{/* Match selector */} {file.matches.length > 0 && ( <>

{`${selectedMatchIndex + 1} of ${ranges.length}`}

)} {/* Close button */}
{ editorRef?.view && file?.filepath && repoName && currentSelection && ( ) } {editorRef && hasCodeNavEntitlement && ( )}
) }