1
1
'use client' ;
2
2
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 ;
50
38
}
51
39
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 ,
67
45
} ) ;
46
+ } , [ repoInfoResponse ] ) ;
68
47
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
+ }
135
56
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
+ }
187
60
188
61
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 ) }
209
84
/>
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
+ </ >
224
98
)
225
- }
226
-
99
+ }
0 commit comments