2024-09-03 01:46:43 +00:00
'use client' ;
import {
ResizableHandle ,
ResizablePanel ,
ResizablePanelGroup ,
} from "@/components/ui/resizable" ;
import { Separator } from "@/components/ui/separator" ;
2024-10-28 17:30:29 +00:00
import useCaptureEvent from "@/hooks/useCaptureEvent" ;
2024-09-03 01:46:43 +00:00
import { useNonEmptyQueryParam } from "@/hooks/useNonEmptyQueryParam" ;
2024-10-28 17:30:29 +00:00
import { SearchQueryParams , SearchResultFile } from "@/lib/types" ;
2024-09-26 03:12:20 +00:00
import { createPathWithQueryParams } from "@/lib/utils" ;
2024-09-03 01:46:43 +00:00
import { SymbolIcon } from "@radix-ui/react-icons" ;
import { useQuery } from "@tanstack/react-query" ;
import Image from "next/image" ;
2024-09-10 19:59:42 +00:00
import { useRouter } from "next/navigation" ;
2024-10-28 17:30:29 +00:00
import { useCallback , useEffect , useMemo , useRef , useState } from "react" ;
2024-09-03 01:46:43 +00:00
import logoDark from "../../../public/sb_logo_dark.png" ;
import logoLight from "../../../public/sb_logo_light.png" ;
2024-09-26 03:12:20 +00:00
import { search } from "../api/(client)/client" ;
2024-09-03 01:46:43 +00:00
import { SearchBar } from "../searchBar" ;
import { SettingsDropdown } from "../settingsDropdown" ;
2024-09-26 03:12:20 +00:00
import { CodePreviewPanel } from "./components/codePreviewPanel" ;
2024-10-28 17:30:29 +00:00
import { FilterPanel } from "./components/filterPanel" ;
2024-09-26 03:12:20 +00:00
import { SearchResultsPanel } from "./components/searchResultsPanel" ;
2024-10-28 17:30:29 +00:00
import { ImperativePanelHandle } from "react-resizable-panels" ;
2024-09-26 06:31:51 +00:00
2024-10-30 16:32:05 +00:00
const DEFAULT_MAX_MATCH_DISPLAY_COUNT = 10000 ;
2024-09-26 06:31:51 +00:00
2024-09-03 01:46:43 +00:00
export default function SearchPage() {
const router = useRouter ( ) ;
2024-09-26 06:31:51 +00:00
const searchQuery = useNonEmptyQueryParam ( SearchQueryParams . query ) ? ? "" ;
const _maxMatchDisplayCount = parseInt ( useNonEmptyQueryParam ( SearchQueryParams . maxMatchDisplayCount ) ? ? ` ${ DEFAULT_MAX_MATCH_DISPLAY_COUNT } ` ) ;
const maxMatchDisplayCount = isNaN ( _maxMatchDisplayCount ) ? DEFAULT_MAX_MATCH_DISPLAY_COUNT : _maxMatchDisplayCount ;
2024-09-03 01:46:43 +00:00
2024-09-17 04:37:34 +00:00
const captureEvent = useCaptureEvent ( ) ;
2024-09-03 01:46:43 +00:00
const { data : searchResponse , isLoading } = useQuery ( {
2024-09-26 06:31:51 +00:00
queryKey : [ "search" , searchQuery , maxMatchDisplayCount ] ,
2024-09-10 06:16:41 +00:00
queryFn : ( ) = > search ( {
query : searchQuery ,
2024-09-26 06:31:51 +00:00
maxMatchDisplayCount ,
2024-09-10 06:16:41 +00:00
} ) ,
2024-09-03 01:46:43 +00:00
enabled : searchQuery.length > 0 ,
2024-09-17 04:37:34 +00:00
refetchOnWindowFocus : false ,
2024-09-03 01:46:43 +00:00
} ) ;
2024-09-17 04:37:34 +00:00
useEffect ( ( ) = > {
if ( ! searchResponse ) {
return ;
}
const fileLanguages = searchResponse . Result . Files ? . map ( file = > file . Language ) || [ ] ;
captureEvent ( "search_finished" , {
contentBytesLoaded : searchResponse.Result.ContentBytesLoaded ,
indexBytesLoaded : searchResponse.Result.IndexBytesLoaded ,
crashes : searchResponse.Result.Crashes ,
durationMs : searchResponse.Result.Duration / 1000000 ,
fileCount : searchResponse.Result.FileCount ,
shardFilesConsidered : searchResponse.Result.ShardFilesConsidered ,
filesConsidered : searchResponse.Result.FilesConsidered ,
filesLoaded : searchResponse.Result.FilesLoaded ,
filesSkipped : searchResponse.Result.FilesSkipped ,
shardsScanned : searchResponse.Result.ShardsScanned ,
shardsSkipped : searchResponse.Result.ShardsSkipped ,
shardsSkippedFilter : searchResponse.Result.ShardsSkippedFilter ,
matchCount : searchResponse.Result.MatchCount ,
ngramMatches : searchResponse.Result.NgramMatches ,
ngramLookups : searchResponse.Result.NgramLookups ,
wait : searchResponse.Result.Wait ,
matchTreeConstruction : searchResponse.Result.MatchTreeConstruction ,
matchTreeSearch : searchResponse.Result.MatchTreeSearch ,
regexpsConsidered : searchResponse.Result.RegexpsConsidered ,
flushReason : searchResponse.Result.FlushReason ,
fileLanguages ,
} ) ;
} , [ captureEvent , searchResponse ] ) ;
2024-09-10 06:16:41 +00:00
const { fileMatches , searchDurationMs } = useMemo ( ( ) : { fileMatches : SearchResultFile [ ] , searchDurationMs : number } = > {
2024-09-03 01:46:43 +00:00
if ( ! searchResponse ) {
return {
fileMatches : [ ] ,
searchDurationMs : 0 ,
} ;
}
2024-09-10 06:16:41 +00:00
2024-09-03 01:46:43 +00:00
return {
2024-09-10 06:16:41 +00:00
fileMatches : searchResponse.Result.Files ? ? [ ] ,
searchDurationMs : Math.round ( searchResponse . Result . Duration / 1000000 ) ,
2024-09-03 01:46:43 +00:00
}
} , [ searchResponse ] ) ;
2024-09-10 19:59:42 +00:00
const isMoreResultsButtonVisible = useMemo ( ( ) = > {
2024-09-26 06:31:51 +00:00
return searchResponse && searchResponse . Result . MatchCount > maxMatchDisplayCount ;
} , [ searchResponse , maxMatchDisplayCount ] ) ;
const numMatches = useMemo ( ( ) = > {
// Accumualtes the number of matches across all files
return searchResponse ? . Result . Files ? . reduce (
( acc , file ) = >
acc + file . ChunkMatches . reduce (
( acc , chunk ) = > acc + chunk . Ranges . length ,
0 ,
) ,
0 ,
) ? ? 0 ;
} , [ searchResponse ] ) ;
const onLoadMoreResults = useCallback ( ( ) = > {
const url = createPathWithQueryParams ( '/search' ,
[ SearchQueryParams . query , searchQuery ] ,
[ SearchQueryParams . maxMatchDisplayCount , ` ${ maxMatchDisplayCount * 2 } ` ] ,
)
router . push ( url ) ;
} , [ maxMatchDisplayCount , router , searchQuery ] ) ;
2024-09-03 01:46:43 +00:00
return (
< div className = "flex flex-col h-screen overflow-clip" >
{ /* TopBar */ }
< div className = "sticky top-0 left-0 right-0 z-10" >
< div className = "flex flex-row justify-between items-center py-1.5 px-3 gap-4" >
< div className = "grow flex flex-row gap-4 items-center" >
< div
2024-09-24 05:30:58 +00:00
className = "shrink-0 cursor-pointer"
2024-09-03 01:46:43 +00:00
onClick = { ( ) = > {
router . push ( "/" ) ;
} }
>
< Image
src = { logoDark }
className = "h-4 w-auto hidden dark:block"
alt = { "Sourcebot logo" }
/ >
< Image
src = { logoLight }
className = "h-4 w-auto block dark:hidden"
alt = { "Sourcebot logo" }
/ >
< / div >
< SearchBar
size = "sm"
defaultQuery = { searchQuery }
/ >
< / div >
< SettingsDropdown
menuButtonClassName = "w-8 h-8"
/ >
< / div >
< Separator / >
2024-10-28 17:30:29 +00:00
{ ! isLoading && (
< div className = "bg-accent py-1 px-2 flex flex-row items-center gap-4" >
{
fileMatches . length > 0 && searchResponse ? (
< p className = "text-sm font-medium" > { ` [ ${ searchDurationMs } ms] Displaying ${ numMatches } of ${ searchResponse . Result . MatchCount } matches in ${ fileMatches . length } ${ fileMatches . length > 1 ? 'files' : 'file' } ` } < / p >
) : (
< p className = "text-sm font-medium" > No results < / p >
)
}
{ isMoreResultsButtonVisible && (
< div
className = "cursor-pointer text-blue-500 text-sm hover:underline"
onClick = { onLoadMoreResults }
>
( load more )
< / div >
) }
< / div >
) }
2024-09-03 01:46:43 +00:00
< Separator / >
< / div >
2024-10-28 17:30:29 +00:00
{ isLoading ? (
< div className = "flex flex-col items-center justify-center h-full gap-2" >
< SymbolIcon className = "h-6 w-6 animate-spin" / >
< p className = "font-semibold text-center" > Searching . . . < / p >
< / div >
) : (
< PanelGroup
fileMatches = { fileMatches }
isMoreResultsButtonVisible = { isMoreResultsButtonVisible }
onLoadMoreResults = { onLoadMoreResults }
/ >
) }
2024-09-03 01:46:43 +00:00
< / div >
) ;
}
2024-10-28 17:30:29 +00:00
interface PanelGroupProps {
fileMatches : SearchResultFile [ ] ;
isMoreResultsButtonVisible? : boolean ;
onLoadMoreResults : ( ) = > void ;
}
const PanelGroup = ( {
fileMatches ,
isMoreResultsButtonVisible ,
onLoadMoreResults ,
} : PanelGroupProps ) = > {
const [ selectedMatchIndex , setSelectedMatchIndex ] = useState ( 0 ) ;
const [ selectedFile , setSelectedFile ] = useState < SearchResultFile | undefined > ( undefined ) ;
const [ filteredFileMatches , setFilteredFileMatches ] = useState < SearchResultFile [ ] > ( fileMatches ) ;
const codePreviewPanelRef = useRef < ImperativePanelHandle > ( null ) ;
useEffect ( ( ) = > {
if ( selectedFile ) {
codePreviewPanelRef . current ? . expand ( ) ;
} else {
codePreviewPanelRef . current ? . collapse ( ) ;
}
} , [ selectedFile ] ) ;
2024-10-30 16:32:05 +00:00
const onFilterChanged = useCallback ( ( matches : SearchResultFile [ ] ) = > {
setFilteredFileMatches ( matches ) ;
} , [ ] ) ;
2024-10-28 17:30:29 +00:00
return (
< ResizablePanelGroup
direction = "horizontal"
>
{ /* ~~ Filter panel ~~ */ }
< ResizablePanel
minSize = { 20 }
maxSize = { 30 }
defaultSize = { 20 }
collapsible = { true }
id = { 'filter-panel' }
order = { 1 }
>
< FilterPanel
matches = { fileMatches }
2024-10-30 16:32:05 +00:00
onFilterChanged = { onFilterChanged }
2024-10-28 17:30:29 +00:00
/ >
< / ResizablePanel >
< ResizableHandle
className = "bg-accent w-1 transition-colors delay-50 data-[resize-handle-state=drag]:bg-accent-foreground data-[resize-handle-state=hover]:bg-accent-foreground"
/ >
{ /* ~~ Search results ~~ */ }
< ResizablePanel
minSize = { 10 }
id = { 'search-results-panel' }
order = { 2 }
>
{ filteredFileMatches . length > 0 ? (
2024-10-30 16:32:05 +00:00
< SearchResultsPanel
fileMatches = { filteredFileMatches }
onOpenFileMatch = { ( fileMatch ) = > {
setSelectedFile ( fileMatch ) ;
} }
onMatchIndexChanged = { ( matchIndex ) = > {
setSelectedMatchIndex ( matchIndex ) ;
} }
isLoadMoreButtonVisible = { ! ! isMoreResultsButtonVisible }
onLoadMoreButtonClicked = { onLoadMoreResults }
/ >
2024-10-28 17:30:29 +00:00
) : (
< div className = "flex flex-col items-center justify-center h-full" >
< p className = "text-sm text-muted-foreground" > No results found < / p >
< / div >
) }
< / ResizablePanel >
< ResizableHandle
withHandle = { selectedFile !== undefined }
/ >
{ /* ~~ Code preview ~~ */ }
< ResizablePanel
ref = { codePreviewPanelRef }
minSize = { 10 }
collapsible = { true }
id = { 'code-preview-panel' }
order = { 3 }
>
< CodePreviewPanel
fileMatch = { selectedFile }
onClose = { ( ) = > setSelectedFile ( undefined ) }
selectedMatchIndex = { selectedMatchIndex }
onSelectedMatchIndexChange = { setSelectedMatchIndex }
/ >
< / ResizablePanel >
< / ResizablePanelGroup >
)
}