1
1
'use client' ;
2
2
3
3
import { isDefined } from "@/lib/utils" ;
4
- import { CommitIcon , MixerVerticalIcon } from "@radix-ui/react-icons" ;
5
- import { IconProps } from "@radix-ui/react-icons/dist/types" ;
6
4
import assert from "assert" ;
7
5
import clsx from "clsx" ;
8
6
import escapeStringRegexp from "escape-string-regexp" ;
@@ -16,13 +14,14 @@ import {
16
14
refineModeSuggestions ,
17
15
suggestionModeMappings
18
16
} from "./constants" ;
19
-
20
- type Icon = React . ForwardRefExoticComponent < IconProps & React . RefAttributes < SVGSVGElement > > ;
17
+ import { IconType } from "react-icons/lib" ;
18
+ import { VscFile , VscFilter , VscRepo , VscSymbolMisc } from "react-icons/vsc" ;
21
19
22
20
export type Suggestion = {
23
21
value : string ;
24
22
description ?: string ;
25
23
spotlight ?: boolean ;
24
+ Icon ?: IconType ;
26
25
}
27
26
28
27
export type SuggestionMode =
@@ -50,30 +49,42 @@ interface SearchSuggestionsBoxProps {
50
49
onSuggestionModeChanged : ( suggestionMode : SuggestionMode ) => void ;
51
50
onSuggestionQueryChanged : ( suggestionQuery : string ) => void ;
52
51
53
- data : {
54
- repos : Suggestion [ ] ;
55
- languages : Suggestion [ ] ;
56
- files : Suggestion [ ] ;
57
- }
52
+ isLoadingSuggestions : boolean ;
53
+ repoSuggestions : Suggestion [ ] ;
54
+ fileSuggestions : Suggestion [ ] ;
55
+ symbolSuggestions : Suggestion [ ] ;
56
+ languageSuggestions : Suggestion [ ] ;
58
57
}
59
58
60
59
const SearchSuggestionsBox = forwardRef ( ( {
61
60
query,
62
61
onCompletion,
63
62
isEnabled,
64
- data,
65
63
cursorPosition,
66
64
isFocused,
67
65
onFocus,
68
66
onBlur,
69
67
onReturnFocus,
70
68
onSuggestionModeChanged,
71
69
onSuggestionQueryChanged,
70
+ isLoadingSuggestions,
71
+ repoSuggestions,
72
+ fileSuggestions,
73
+ symbolSuggestions,
74
+ languageSuggestions,
72
75
} : SearchSuggestionsBoxProps , ref : Ref < HTMLDivElement > ) => {
73
76
74
77
const [ highlightedSuggestionIndex , setHighlightedSuggestionIndex ] = useState ( 0 ) ;
75
78
76
79
const { suggestionQuery, suggestionMode } = useMemo < { suggestionQuery ?: string , suggestionMode ?: SuggestionMode } > ( ( ) => {
80
+ // Only re-calculate the suggestion mode and query if the box is enabled.
81
+ // This is to avoid transitioning the suggestion mode and causing a fetch
82
+ // when it is not needed.
83
+ // @see : useSuggestionsData.ts
84
+ if ( ! isEnabled ) {
85
+ return { } ;
86
+ }
87
+
77
88
const { queryParts, cursorIndex } = splitQuery ( query , cursorPosition ) ;
78
89
if ( queryParts . length === 0 ) {
79
90
return { } ;
@@ -107,10 +118,10 @@ const SearchSuggestionsBox = forwardRef(({
107
118
suggestionQuery : part ,
108
119
suggestionMode : "refine" ,
109
120
}
110
- } , [ cursorPosition , query ] ) ;
121
+ } , [ cursorPosition , isEnabled , query ] ) ;
111
122
112
- const { suggestions, isHighlightEnabled, Icon , onSuggestionClicked } = useMemo ( ( ) => {
113
- if ( ! isDefined ( suggestionQuery ) || ! isDefined ( suggestionMode ) ) {
123
+ const { suggestions, isHighlightEnabled, DefaultIcon , onSuggestionClicked } = useMemo ( ( ) => {
124
+ if ( ! isEnabled || ! isDefined ( suggestionQuery ) || ! isDefined ( suggestionMode ) ) {
114
125
return { } ;
115
126
}
116
127
@@ -144,7 +155,7 @@ const SearchSuggestionsBox = forwardRef(({
144
155
isSpotlightEnabled = false ,
145
156
isClientSideSearchEnabled = true ,
146
157
onSuggestionClicked,
147
- Icon ,
158
+ DefaultIcon ,
148
159
} = ( ( ) : {
149
160
threshold ?: number ,
150
161
limit ?: number ,
@@ -153,7 +164,7 @@ const SearchSuggestionsBox = forwardRef(({
153
164
isSpotlightEnabled ?: boolean ,
154
165
isClientSideSearchEnabled ?: boolean ,
155
166
onSuggestionClicked : ( value : string ) => void ,
156
- Icon ?: Icon
167
+ DefaultIcon ?: IconType
157
168
} => {
158
169
switch ( suggestionMode ) {
159
170
case "public" :
@@ -178,13 +189,13 @@ const SearchSuggestionsBox = forwardRef(({
178
189
}
179
190
case "repo" :
180
191
return {
181
- list : data . repos ,
182
- Icon : CommitIcon ,
192
+ list : repoSuggestions ,
193
+ DefaultIcon : VscRepo ,
183
194
onSuggestionClicked : createOnSuggestionClickedHandler ( { regexEscaped : true } ) ,
184
195
}
185
196
case "language" : {
186
197
return {
187
- list : data . languages ,
198
+ list : languageSuggestions ,
188
199
onSuggestionClicked : createOnSuggestionClickedHandler ( ) ,
189
200
isSpotlightEnabled : true ,
190
201
}
@@ -195,18 +206,25 @@ const SearchSuggestionsBox = forwardRef(({
195
206
list : refineModeSuggestions ,
196
207
isHighlightEnabled : true ,
197
208
isSpotlightEnabled : true ,
198
- Icon : MixerVerticalIcon ,
209
+ DefaultIcon : VscFilter ,
199
210
onSuggestionClicked : createOnSuggestionClickedHandler ( { trailingSpace : false } ) ,
200
211
}
201
212
case "file" :
202
213
return {
203
- list : data . files ,
214
+ list : fileSuggestions ,
215
+ onSuggestionClicked : createOnSuggestionClickedHandler ( ) ,
216
+ isClientSideSearchEnabled : false ,
217
+ DefaultIcon : VscFile ,
218
+ }
219
+ case "symbol" :
220
+ return {
221
+ list : symbolSuggestions ,
204
222
onSuggestionClicked : createOnSuggestionClickedHandler ( ) ,
205
223
isClientSideSearchEnabled : false ,
224
+ DefaultIcon : VscSymbolMisc ,
206
225
}
207
226
case "revision" :
208
227
case "content" :
209
- case "symbol" :
210
228
return {
211
229
list : [ ] ,
212
230
onSuggestionClicked : createOnSuggestionClickedHandler ( ) ,
@@ -252,11 +270,11 @@ const SearchSuggestionsBox = forwardRef(({
252
270
return {
253
271
suggestions,
254
272
isHighlightEnabled,
255
- Icon ,
273
+ DefaultIcon ,
256
274
onSuggestionClicked,
257
275
}
258
276
259
- } , [ suggestionQuery , suggestionMode , query , cursorPosition , onCompletion , data . repos , data . files , data . languages ] ) ;
277
+ } , [ isEnabled , suggestionQuery , suggestionMode , query , cursorPosition , onCompletion , repoSuggestions , fileSuggestions , symbolSuggestions , languageSuggestions ] ) ;
260
278
261
279
// When the list of suggestions change, reset the highlight index
262
280
useEffect ( ( ) => {
@@ -283,20 +301,29 @@ const SearchSuggestionsBox = forwardRef(({
283
301
case "repo" :
284
302
return "Repositories" ;
285
303
case "refine" :
286
- return "Refine search"
304
+ return "Refine search" ;
305
+ case "file" :
306
+ return "Files" ;
307
+ case "symbol" :
308
+ return "Symbols" ;
309
+ case "language" :
310
+ return "Languages" ;
287
311
default :
288
312
return "" ;
289
313
}
290
314
} , [ suggestionMode ] ) ;
291
315
292
316
if (
293
317
! isEnabled ||
294
- ! suggestions ||
295
- suggestions . length === 0
318
+ ! suggestions
296
319
) {
297
320
return null ;
298
321
}
299
322
323
+ if ( suggestions . length === 0 && ! isLoadingSuggestions ) {
324
+ return null ;
325
+ }
326
+
300
327
return (
301
328
< div
302
329
ref = { ref }
@@ -305,6 +332,9 @@ const SearchSuggestionsBox = forwardRef(({
305
332
onKeyDown = { ( e ) => {
306
333
if ( e . key === 'Enter' ) {
307
334
e . stopPropagation ( ) ;
335
+ if ( highlightedSuggestionIndex < 0 || highlightedSuggestionIndex >= suggestions . length ) {
336
+ return ;
337
+ }
308
338
const value = suggestions [ highlightedSuggestionIndex ] . value ;
309
339
onSuggestionClicked ( value ) ;
310
340
}
@@ -334,7 +364,17 @@ const SearchSuggestionsBox = forwardRef(({
334
364
< p className = "text-muted-foreground text-sm mb-1" >
335
365
{ suggestionModeText }
336
366
</ p >
337
- { suggestions . map ( ( result , index ) => (
367
+ { isLoadingSuggestions ? (
368
+ // Skeleton placeholder
369
+ < div className = "animate-pulse flex flex-col gap-2 px-1 py-0.5" >
370
+ {
371
+ Array . from ( { length : 10 } ) . map ( ( _ , index ) => (
372
+ < div key = { index } className = "h-4 bg-muted rounded-md w-full" > </ div >
373
+ ) )
374
+ }
375
+ </ div >
376
+ ) : suggestions . map ( ( result , index ) => (
377
+ // Suggestion list
338
378
< div
339
379
key = { index }
340
380
className = { clsx ( "flex flex-row items-center font-mono text-sm hover:bg-muted rounded-md px-1 py-0.5 cursor-pointer" , {
@@ -345,23 +385,24 @@ const SearchSuggestionsBox = forwardRef(({
345
385
onSuggestionClicked ( result . value )
346
386
} }
347
387
>
348
- { Icon && (
349
- < Icon className = "w-3 h-3 mr-2" />
350
- ) }
351
- < div className = "flex flex-row items-center" >
352
- < span
353
- className = { clsx ( 'mr-2 flex-none' , {
354
- "text-highlight" : isHighlightEnabled
355
- } ) }
356
- >
357
- { result . value }
388
+ { result . Icon ? (
389
+ < result . Icon className = "w-3 h-3 mr-2 flex-none" />
390
+ ) : DefaultIcon ? (
391
+ < DefaultIcon className = "w-3 h-3 mr-2 flex-none" />
392
+ ) : null }
393
+ < span
394
+ className = { clsx ( 'mr-2' , {
395
+ "text-highlight" : isHighlightEnabled ,
396
+ "truncate" : ! result . description ,
397
+ } ) }
398
+ >
399
+ { result . value }
400
+ </ span >
401
+ { result . description && (
402
+ < span className = "text-muted-foreground font-light" >
403
+ { result . description }
358
404
</ span >
359
- { result . description && (
360
- < span className = "text-muted-foreground font-light" >
361
- { result . description }
362
- </ span >
363
- ) }
364
- </ div >
405
+ ) }
365
406
</ div >
366
407
) ) }
367
408
{ isFocused && (
0 commit comments