11'use client' ;
22
3- import { ResizablePanel } from "@/components/ui/resizable" ;
4- import { ScrollArea } from "@/components/ui/scroll-area" ;
5- import { SymbolHoverPopup } from "@/ee/features/codeNav/components/symbolHoverPopup" ;
6- import { symbolHoverTargetsExtension } from "@/ee/features/codeNav/components/symbolHoverPopup/symbolHoverTargetsExtension" ;
7- import { SymbolDefinition } from "@/ee/features/codeNav/components/symbolHoverPopup/useHoveredOverSymbolInfo" ;
8- import { useHasEntitlement } from "@/features/entitlements/useHasEntitlement" ;
9- import { useCodeMirrorLanguageExtension } from "@/hooks/useCodeMirrorLanguageExtension" ;
10- import { useCodeMirrorTheme } from "@/hooks/useCodeMirrorTheme" ;
11- import { useKeymapExtension } from "@/hooks/useKeymapExtension" ;
12- import { useNonEmptyQueryParam } from "@/hooks/useNonEmptyQueryParam" ;
13- import { search } from "@codemirror/search" ;
14- import CodeMirror , { EditorSelection , EditorView , ReactCodeMirrorRef , SelectionRange , ViewUpdate } from "@uiw/react-codemirror" ;
15- import { useCallback , useEffect , useMemo , useState } from "react" ;
16- import { EditorContextMenu } from "../../../components/editorContextMenu" ;
17- import { BrowseHighlightRange , HIGHLIGHT_RANGE_QUERY_PARAM , useBrowseNavigation } from "../../hooks/useBrowseNavigation" ;
18- import { useBrowseState } from "../../hooks/useBrowseState" ;
19- import { rangeHighlightingExtension } from "./rangeHighlightingExtension" ;
20- import useCaptureEvent from "@/hooks/useCaptureEvent" ;
21-
22- interface CodePreviewPanelProps {
23- path : string ;
24- repoName : string ;
25- revisionName : string ;
26- source : string ;
27- language : string ;
28- }
29-
30- export const CodePreviewPanel = ( {
31- source,
32- language,
33- path,
34- repoName,
35- revisionName,
36- } : CodePreviewPanelProps ) => {
37- const [ editorRef , setEditorRef ] = useState < ReactCodeMirrorRef | null > ( null ) ;
38- const languageExtension = useCodeMirrorLanguageExtension ( language , editorRef ?. view ) ;
39- const [ currentSelection , setCurrentSelection ] = useState < SelectionRange > ( ) ;
40- const keymapExtension = useKeymapExtension ( editorRef ?. view ) ;
41- const hasCodeNavEntitlement = useHasEntitlement ( "code-nav" ) ;
42- const { updateBrowseState } = useBrowseState ( ) ;
43- const { navigateToPath } = useBrowseNavigation ( ) ;
44- const captureEvent = useCaptureEvent ( ) ;
45-
46- const highlightRangeQuery = useNonEmptyQueryParam ( HIGHLIGHT_RANGE_QUERY_PARAM ) ;
47- const highlightRange = useMemo ( ( ) : BrowseHighlightRange | undefined => {
48- if ( ! highlightRangeQuery ) {
49- return ;
3+ import { base64Decode , getCodeHostInfoForRepo , unwrapServiceError } from "@/lib/utils" ;
4+ import { useBrowseParams } from "@/app/[domain]/browse/hooks/useBrowseParams" ;
5+ import { useQuery } from "@tanstack/react-query" ;
6+ import { getFileSource } from "@/features/search/fileSourceApi" ;
7+ import { useDomain } from "@/hooks/useDomain" ;
8+ import { Loader2 } from "lucide-react" ;
9+ import { Separator } from "@/components/ui/separator" ;
10+ import { getRepoInfoByName } from "@/actions" ;
11+ import { cn } from "@/lib/utils" ;
12+ import Image from "next/image" ;
13+ import { useMemo } from "react" ;
14+ import { PureCodePreviewPanel } from "./pureCodePreviewPanel" ;
15+ import { PathHeader } from "@/app/[domain]/components/pathHeader" ;
16+
17+ export const CodePreviewPanel = ( ) => {
18+ const { path, repoName, revisionName } = useBrowseParams ( ) ;
19+ const domain = useDomain ( ) ;
20+
21+ const { data : fileSourceResponse , isPending : isFileSourcePending , isError : isFileSourceError } = useQuery ( {
22+ queryKey : [ 'fileSource' , repoName , revisionName , path , domain ] ,
23+ queryFn : ( ) => unwrapServiceError ( getFileSource ( {
24+ fileName : path ,
25+ repository : repoName ,
26+ branch : revisionName
27+ } , domain ) ) ,
28+ } ) ;
29+
30+ const { data : repoInfoResponse , isPending : isRepoInfoPending , isError : isRepoInfoError } = useQuery ( {
31+ queryKey : [ 'repoInfo' , repoName , domain ] ,
32+ queryFn : ( ) => unwrapServiceError ( getRepoInfoByName ( repoName , domain ) ) ,
33+ } ) ;
34+
35+ const codeHostInfo = useMemo ( ( ) => {
36+ if ( ! repoInfoResponse ) {
37+ return undefined ;
5038 }
5139
52- // Highlight ranges can be formatted in two ways:
53- // 1. start_line,end_line (no column specified)
54- // 2. start_line:start_column,end_line:end_column (column specified)
55- const rangeRegex = / ^ ( \d + : \d + , \d + : \d + | \d + , \d + ) $ / ;
56- if ( ! rangeRegex . test ( highlightRangeQuery ) ) {
57- return ;
58- }
59-
60- const [ start , end ] = highlightRangeQuery . split ( ',' ) . map ( ( range ) => {
61- if ( range . includes ( ':' ) ) {
62- return range . split ( ':' ) . map ( ( val ) => parseInt ( val , 10 ) ) ;
63- }
64- // For line-only format, use column 1 for start and last column for end
65- const line = parseInt ( range , 10 ) ;
66- return [ line ] ;
40+ return getCodeHostInfoForRepo ( {
41+ codeHostType : repoInfoResponse . codeHostType ,
42+ name : repoInfoResponse . name ,
43+ displayName : repoInfoResponse . displayName ,
44+ webUrl : repoInfoResponse . webUrl ,
6745 } ) ;
46+ } , [ repoInfoResponse ] ) ;
6847
69- if ( start . length === 1 || end . length === 1 ) {
70- return {
71- start : {
72- lineNumber : start [ 0 ] ,
73- } ,
74- end : {
75- lineNumber : end [ 0 ] ,
76- }
77- }
78- } else {
79- return {
80- start : {
81- lineNumber : start [ 0 ] ,
82- column : start [ 1 ] ,
83- } ,
84- end : {
85- lineNumber : end [ 0 ] ,
86- column : end [ 1 ] ,
87- }
88- }
89- }
90-
91- } , [ highlightRangeQuery ] ) ;
92-
93- const extensions = useMemo ( ( ) => {
94- return [
95- languageExtension ,
96- EditorView . lineWrapping ,
97- keymapExtension ,
98- search ( {
99- top : true ,
100- } ) ,
101- EditorView . updateListener . of ( ( update : ViewUpdate ) => {
102- if ( update . selectionSet ) {
103- setCurrentSelection ( update . state . selection . main ) ;
104- }
105- } ) ,
106- highlightRange ? rangeHighlightingExtension ( highlightRange ) : [ ] ,
107- hasCodeNavEntitlement ? symbolHoverTargetsExtension : [ ] ,
108- ] ;
109- } , [
110- keymapExtension ,
111- languageExtension ,
112- highlightRange ,
113- hasCodeNavEntitlement ,
114- ] ) ;
115-
116- // Scroll the highlighted range into view.
117- useEffect ( ( ) => {
118- if ( ! highlightRange || ! editorRef || ! editorRef . state ) {
119- return ;
120- }
121-
122- const doc = editorRef . state . doc ;
123- const { start, end } = highlightRange ;
124- const selection = EditorSelection . range (
125- doc . line ( start . lineNumber ) . from ,
126- doc . line ( end . lineNumber ) . from ,
127- ) ;
128-
129- editorRef . view ?. dispatch ( {
130- effects : [
131- EditorView . scrollIntoView ( selection , { y : "center" } ) ,
132- ]
133- } ) ;
134- } , [ editorRef , highlightRange ] ) ;
48+ if ( isFileSourcePending || isRepoInfoPending ) {
49+ return (
50+ < div className = "flex flex-col w-full min-h-full items-center justify-center" >
51+ < Loader2 className = "w-4 h-4 animate-spin" />
52+ Loading...
53+ </ div >
54+ )
55+ }
13556
136- const onFindReferences = useCallback ( ( symbolName : string ) => {
137- captureEvent ( 'wa_browse_find_references_pressed' , { } ) ;
138-
139- updateBrowseState ( {
140- selectedSymbolInfo : {
141- repoName,
142- symbolName,
143- revisionName,
144- language,
145- } ,
146- isBottomPanelCollapsed : false ,
147- activeExploreMenuTab : "references" ,
148- } )
149- } , [ captureEvent , updateBrowseState , repoName , revisionName , language ] ) ;
150-
151-
152- // If we resolve multiple matches, instead of navigating to the first match, we should
153- // instead popup the bottom sheet with the list of matches.
154- const onGotoDefinition = useCallback ( ( symbolName : string , symbolDefinitions : SymbolDefinition [ ] ) => {
155- captureEvent ( 'wa_browse_goto_definition_pressed' , { } ) ;
156-
157- if ( symbolDefinitions . length === 0 ) {
158- return ;
159- }
160-
161- if ( symbolDefinitions . length === 1 ) {
162- const symbolDefinition = symbolDefinitions [ 0 ] ;
163- const { fileName, repoName } = symbolDefinition ;
164-
165- navigateToPath ( {
166- repoName,
167- revisionName,
168- path : fileName ,
169- pathType : 'blob' ,
170- highlightRange : symbolDefinition . range ,
171- } )
172- } else {
173- updateBrowseState ( {
174- selectedSymbolInfo : {
175- symbolName,
176- repoName,
177- revisionName,
178- language,
179- } ,
180- activeExploreMenuTab : "definitions" ,
181- isBottomPanelCollapsed : false ,
182- } )
183- }
184- } , [ captureEvent , navigateToPath , revisionName , updateBrowseState , repoName , language ] ) ;
185-
186- const theme = useCodeMirrorTheme ( ) ;
57+ if ( isFileSourceError || isRepoInfoError ) {
58+ return < div > Error loading file source</ div >
59+ }
18760
18861 return (
189- < ResizablePanel
190- order = { 1 }
191- id = { "code-preview-panel" }
192- >
193- < ScrollArea className = "h-full overflow-auto flex-1" >
194- < CodeMirror
195- className = "relative"
196- ref = { setEditorRef }
197- value = { source }
198- extensions = { extensions }
199- readOnly = { true }
200- theme = { theme }
201- >
202- { editorRef && editorRef . view && currentSelection && (
203- < EditorContextMenu
204- view = { editorRef . view }
205- selection = { currentSelection }
206- repoName = { repoName }
207- path = { path }
208- revisionName = { revisionName }
62+ < >
63+ < div className = "flex flex-row py-1 px-2 items-center justify-between" >
64+ < PathHeader
65+ path = { path }
66+ repo = { {
67+ name : repoName ,
68+ codeHostType : repoInfoResponse . codeHostType ,
69+ displayName : repoInfoResponse . displayName ,
70+ webUrl : repoInfoResponse . webUrl ,
71+ } }
72+ />
73+ { ( fileSourceResponse . webUrl && codeHostInfo ) && (
74+ < a
75+ href = { fileSourceResponse . webUrl }
76+ target = "_blank"
77+ rel = "noopener noreferrer"
78+ className = "flex flex-row items-center gap-2 px-2 py-0.5 rounded-md flex-shrink-0"
79+ >
80+ < Image
81+ src = { codeHostInfo . icon }
82+ alt = { codeHostInfo . codeHostName }
83+ className = { cn ( 'w-4 h-4 flex-shrink-0' , codeHostInfo . iconClassName ) }
20984 />
210- ) }
211- { editorRef && hasCodeNavEntitlement && (
212- < SymbolHoverPopup
213- editorRef = { editorRef }
214- revisionName = { revisionName }
215- language = { language }
216- onFindReferences = { onFindReferences }
217- onGotoDefinition = { onGotoDefinition }
218- />
219- ) }
220- </ CodeMirror >
221-
222- </ ScrollArea >
223- </ ResizablePanel >
85+ < span className = "text-sm font-medium" > Open in { codeHostInfo . codeHostName } </ span >
86+ </ a >
87+ ) }
88+ </ div >
89+ < Separator />
90+ < PureCodePreviewPanel
91+ source = { base64Decode ( fileSourceResponse . source ) }
92+ language = { fileSourceResponse . language }
93+ repoName = { repoName }
94+ path = { path }
95+ revisionName = { revisionName ?? 'HEAD' }
96+ />
97+ </ >
22498 )
225- }
226-
99+ }
0 commit comments