diff --git a/src/app/search/components/searchResultsPanel/index.tsx b/src/app/search/components/searchResultsPanel/index.tsx index 023f6bf3..128ab9b2 100644 --- a/src/app/search/components/searchResultsPanel/index.tsx +++ b/src/app/search/components/searchResultsPanel/index.tsx @@ -1,9 +1,7 @@ 'use client'; -import { ScrollArea } from "@/components/ui/scroll-area"; -import { Scrollbar } from "@radix-ui/react-scroll-area"; -import { FileMatchContainer } from "./fileMatchContainer"; import { SearchResultFile } from "@/lib/types"; +import { FileMatchContainer } from "./fileMatchContainer"; interface SearchResultsPanelProps { fileMatches: SearchResultFile[]; @@ -16,32 +14,16 @@ export const SearchResultsPanel = ({ onOpenFileMatch, onMatchIndexChanged, }: SearchResultsPanelProps) => { - - if (fileMatches.length === 0) { - return ( -
-

No results found

-
- ); - } - - return ( - - {fileMatches.map((fileMatch, index) => ( - { - onOpenFileMatch(fileMatch); - }} - onMatchIndexChanged={(matchIndex) => { - onMatchIndexChanged(matchIndex); - }} - /> - ))} - - - ) + return fileMatches.map((fileMatch, index) => ( + { + onOpenFileMatch(fileMatch); + }} + onMatchIndexChanged={(matchIndex) => { + onMatchIndexChanged(matchIndex); + }} + /> + )) } \ No newline at end of file diff --git a/src/app/search/page.tsx b/src/app/search/page.tsx index 0690f8c1..ce0981e6 100644 --- a/src/app/search/page.tsx +++ b/src/app/search/page.tsx @@ -12,7 +12,7 @@ import { SymbolIcon } from "@radix-ui/react-icons"; import { useQuery } from "@tanstack/react-query"; import Image from "next/image"; import { useRouter } from "next/navigation"; -import { useEffect, useMemo, useState } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; import logoDark from "../../../public/sb_logo_dark.png"; import logoLight from "../../../public/sb_logo_light.png"; import { search } from "../api/(client)/client"; @@ -22,14 +22,22 @@ import useCaptureEvent from "@/hooks/useCaptureEvent"; import { CodePreviewPanel } from "./components/codePreviewPanel"; import { SearchResultsPanel } from "./components/searchResultsPanel"; import { SearchResultFile } from "@/lib/types"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { Scrollbar } from "@radix-ui/react-scroll-area"; + +const DEFAULT_MAX_MATCH_DISPLAY_COUNT = 200; + +export enum SearchQueryParams { + query = "query", + maxMatchDisplayCount = "maxMatchDisplayCount", +} -const DEFAULT_NUM_RESULTS = 100; export default function SearchPage() { const router = useRouter(); - const searchQuery = useNonEmptyQueryParam("query") ?? ""; - const _numResults = parseInt(useNonEmptyQueryParam("numResults") ?? `${DEFAULT_NUM_RESULTS}`); - const numResults = isNaN(_numResults) ? DEFAULT_NUM_RESULTS : _numResults; + 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; const [selectedMatchIndex, setSelectedMatchIndex] = useState(0); const [selectedFile, setSelectedFile] = useState(undefined); @@ -37,10 +45,10 @@ export default function SearchPage() { const captureEvent = useCaptureEvent(); const { data: searchResponse, isLoading } = useQuery({ - queryKey: ["search", searchQuery, numResults], + queryKey: ["search", searchQuery, maxMatchDisplayCount], queryFn: () => search({ query: searchQuery, - numResults, + maxMatchDisplayCount, }), enabled: searchQuery.length > 0, refetchOnWindowFocus: false, @@ -93,8 +101,28 @@ export default function SearchPage() { }, [searchResponse]); const isMoreResultsButtonVisible = useMemo(() => { - return searchResponse && searchResponse.Result.MatchCount > numResults; - }, [searchResponse, numResults]); + 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]); return (
@@ -129,20 +157,22 @@ export default function SearchPage() { />
-
-

Results for: {fileMatches.length} files in {searchDurationMs} ms

- {isMoreResultsButtonVisible && ( +
+ { + isLoading ? ( +

Loading...

+ ) : fileMatches.length > 0 ? ( +

{`[${searchDurationMs} ms] Displaying ${numMatches} matches in ${fileMatches.length} ${fileMatches.length > 1 ? 'files' : 'file'}`}

+ ) : ( +

No results

+ ) + } + {isMoreResultsButtonVisible && !isLoading && (
{ - const url = createPathWithQueryParams('/search', - ["query", searchQuery], - ["numResults", `${numResults * 2}`], - ) - router.push(url); - }} + onClick={onLoadMoreResults} > - More results + (load more)
)}
@@ -157,16 +187,35 @@ export default function SearchPage() {

Searching...

+ ) : fileMatches.length > 0 ? ( + + { + setSelectedFile(fileMatch); + }} + onMatchIndexChanged={(matchIndex) => { + setSelectedMatchIndex(matchIndex); + }} + /> + {isMoreResultsButtonVisible && ( +
+ + Load more results + +
+ )} + +
) : ( - { - setSelectedFile(fileMatch); - }} - onMatchIndexChanged={(matchIndex) => { - setSelectedMatchIndex(matchIndex); - }} - /> +
+

No results found

+
)} diff --git a/src/app/searchBar.tsx b/src/app/searchBar.tsx index 4a39c8fb..fc3f2fd1 100644 --- a/src/app/searchBar.tsx +++ b/src/app/searchBar.tsx @@ -8,7 +8,7 @@ import { FormMessage, } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; -import { cn } from "@/lib/utils"; +import { cn, createPathWithQueryParams } from "@/lib/utils"; import { zodResolver } from "@hookform/resolvers/zod"; import { cva } from "class-variance-authority"; import { useRouter } from "next/navigation"; @@ -16,6 +16,7 @@ import { useForm } from "react-hook-form"; import { z } from "zod"; import { useHotkeys } from 'react-hotkeys-hook' import { useRef } from "react"; +import { SearchQueryParams } from "./search/page"; interface SearchBarProps { className?: string; @@ -65,7 +66,10 @@ export const SearchBar = ({ }); const onSubmit = (values: z.infer) => { - router.push(`/search?query=${values.query}&numResults=100`); + const url = createPathWithQueryParams('/search', + [SearchQueryParams.query, values.query], + ) + router.push(url); } return ( diff --git a/src/lib/schemas.ts b/src/lib/schemas.ts index 3c012bff..fd2f05a0 100644 --- a/src/lib/schemas.ts +++ b/src/lib/schemas.ts @@ -2,7 +2,7 @@ import { z } from "zod"; export const searchRequestSchema = z.object({ query: z.string(), - numResults: z.number(), + maxMatchDisplayCount: z.number(), whole: z.boolean().optional(), }); diff --git a/src/lib/server/searchService.ts b/src/lib/server/searchService.ts index 2bc57ea1..cb480398 100644 --- a/src/lib/server/searchService.ts +++ b/src/lib/server/searchService.ts @@ -6,14 +6,14 @@ import { fileNotFound, invalidZoektResponse, ServiceError, unexpectedError } fro import { isServiceError } from "../utils"; import { zoektFetch } from "./zoektClient"; -export const search = async ({ query, numResults, whole }: SearchRequest): Promise => { +export const search = async ({ query, maxMatchDisplayCount, whole }: SearchRequest): Promise => { const body = JSON.stringify({ q: query, // @see: https://github.com/TaqlaAI/zoekt/blob/main/api.go#L892 opts: { NumContextLines: 2, ChunkMatches: true, - MaxMatchDisplayCount: numResults, + MaxMatchDisplayCount: maxMatchDisplayCount, Whole: !!whole, ShardMaxMatchCount: SHARD_MAX_MATCH_COUNT, TotalMaxMatchCount: TOTAL_MAX_MATCH_COUNT, @@ -46,7 +46,7 @@ export const getFileSource = async ({ fileName, repository }: FileSourceRequest) const searchResponse = await search({ query: `${escapedFileName} repo:^${escapedRepository}$`, - numResults: 1, + maxMatchDisplayCount: 1, whole: true, });