'use client'; import { Input } from "@/components/ui/input"; import { Separator } from "@/components/ui/separator"; import { useNonEmptyQueryParam } from "@/hooks/useNonEmptyQueryParam"; import { defaultKeymap } from "@codemirror/commands"; import { javascript } from "@codemirror/lang-javascript"; import { EditorView, keymap, ViewPlugin, ViewUpdate } from "@codemirror/view"; import { SymbolIcon, FileIcon, Cross1Icon } from "@radix-ui/react-icons"; import { ScrollArea, Scrollbar } from "@radix-ui/react-scroll-area"; import CodeMirror from '@uiw/react-codemirror'; import Image from "next/image"; import { useRouter } from "next/navigation"; import { useEffect, useMemo, useState } from "react"; import { useDebouncedCallback } from 'use-debounce'; import logoLight from "../../public/sb_logo_light.png"; import logoDark from "../../public/sb_logo_dark.png"; import { ResizableHandle, ResizablePanel, ResizablePanelGroup, } from "@/components/ui/resizable"; import { GetSourceResponse, pathQueryParamName, repoQueryParamName } from "@/lib/api"; import { createPathWithQueryParams } from "@/lib/utils"; import { ThemeSelectorButton } from "./themeSelectorButton"; import { useTheme } from "next-themes"; import { Button } from "@/components/ui/button"; interface ZoekMatch { URL: string, FileName: string, LineNum: number, Fragments: { Pre: string, Match: string, Post: string }[] } interface ZoekFileMatch { FileName: string, Repo: string, Language: string, Matches: ZoekMatch[], URL: string, } interface ZoekResult { QueryStr: string, FileMatches: ZoekFileMatch[] | null, Stats: { // Duration in nanoseconds Duration: number, } } interface ZoekSearchResponse { result: ZoekResult, } export default function Home() { const router = useRouter(); const defaultQuery = useNonEmptyQueryParam("query") ?? ""; const defaultNumResults = useNonEmptyQueryParam("numResults"); const [query, setQuery] = useState(defaultQuery); const [numResults, _setNumResults] = useState(defaultNumResults && !isNaN(Number(defaultNumResults)) ? Number(defaultNumResults) : 100); const [isCodePanelOpen, setIsCodePanelOpen] = useState(false); const [code, setCode] = useState(""); const [filepath, setFilepath] = useState(""); const [fileMatches, setFileMatches] = useState([]); const [isLoading, setIsLoading] = useState(false); const [searchDurationMs, setSearchDurationMs] = useState(0); // @todo: We need to be able to handle the case when the user navigates backwards / forwards. // Currently we do not re-query. return (
{"Sourcebot {"Sourcebot setQuery(query)} onLoadingChange={(isLoading) => setIsLoading(isLoading)} onSearchResult={(result) => { if (result) { setFileMatches(result.FileMatches ?? []); setSearchDurationMs(Math.round(result.Stats.Duration / 1000000)); } router.push(`?query=${query}&numResults=${numResults}`); }} /> {isLoading && ( )}

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

{fileMatches.map((match, index) => ( { const url = createPathWithQueryParams( `http://localhost:3000/api/source`, [pathQueryParamName, match.FileName], [repoQueryParamName, match.Repo] ); // @todo : this query should definitely be cached s.t., when the user is switching between files, // we aren't re-fetching the same file. fetch(url) .then(response => response.json()) .then((body: GetSourceResponse) => { setIsCodePanelOpen(true); setCode(body.content); setFilepath(match.FileName); }); }} /> ))}
{isCodePanelOpen && ( setIsCodePanelOpen(false)} /> )}
); } interface CodeEditorProps { code: string; filepath: string; onClose: () => void; } const CodeEditor = ({ code, filepath, onClose, }: CodeEditorProps) => { const { theme: _theme, systemTheme } = useTheme(); const theme = useMemo(() => { if (_theme === "system") { return systemTheme ?? "light"; } return _theme ?? "light"; }, [_theme]); const [gutterWidth, setGutterWidth] = useState(0); const gutterWidthPlugin = useMemo(() => { return ViewPlugin.fromClass(class { width: number = 0; constructor(view: EditorView) { this.measureWidth(view) } update(update: ViewUpdate) { if (update.geometryChanged) this.measureWidth(update.view) } measureWidth(view: EditorView) { let gutter = view.scrollDOM.querySelector('.cm-gutters') as HTMLElement if (gutter) this.width = gutter.offsetWidth } }); }, []); return (
{filepath}
{ const width = update.view.plugin(gutterWidthPlugin)?.width; if (width) { setGutterWidth(width); } }) ]} />
) } interface SearchBarProps { query: string; numResults: number; onLoadingChange: (isLoading: boolean) => void; onQueryChange: (query: string) => void; onSearchResult: (result?: ZoekResult) => void, } const SearchBar = ({ query, numResults, onLoadingChange, onQueryChange, onSearchResult, }: SearchBarProps) => { const SEARCH_DEBOUNCE_MS = 200; // @todo : we should probably be cancelling any running requests const search = useDebouncedCallback((query: string) => { if (query === "") { onSearchResult(undefined); return; } console.log('making query...'); onLoadingChange(true); fetch(`http://localhost:3000/api/search?query=${query}&numResults=${numResults}`) .then(response => response.json()) .then(({ data }: { data: ZoekSearchResponse }) => { onSearchResult(data.result); }) // @todo : error handling .catch(error => { console.error('Error:', error); }).finally(() => { console.log('done making query'); onLoadingChange(false); }); }, SEARCH_DEBOUNCE_MS); useEffect(() => { search(query); }, [query]); return ( { const query = e.target.value; onQueryChange(query); }} /> ) } interface FileMatchProps { match: ZoekFileMatch; onOpenFile: () => void; } const FileMatch = ({ match, onOpenFile, }: FileMatchProps) => { return (
{match.Repo} ยท {match.FileName}
{match.Matches.map((match, index) => { const fragment = match.Fragments[0]; return (
{ onOpenFile(); }} >

{match.LineNum}: {fragment.Pre}{fragment.Match}{fragment.Post}

); })}
); }