diff --git a/src/app/api/source/route.ts b/src/app/api/source/route.ts index 3f948a9b..4b884590 100644 --- a/src/app/api/source/route.ts +++ b/src/app/api/source/route.ts @@ -6,7 +6,7 @@ import { StatusCodes } from "http-status-codes"; import { NextRequest } from "next/server"; import path from "path"; import fs from "fs"; -import { GetSourceResponse, pathQueryParamName, repoQueryParamName } from "@/lib/api"; +import { GetSourceResponse, pathQueryParamName, repoQueryParamName } from "@/lib/types"; /** diff --git a/src/app/codePreview.tsx b/src/app/codePreview.tsx new file mode 100644 index 00000000..06a66803 --- /dev/null +++ b/src/app/codePreview.tsx @@ -0,0 +1,98 @@ +'use client'; + +import { Button } from "@/components/ui/button"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { EditorView, keymap, ViewPlugin, ViewUpdate } from "@codemirror/view"; +import { Cross1Icon, FileIcon } from "@radix-ui/react-icons"; +import { useTheme } from "next-themes"; +import { useMemo, useState } from "react"; +import CodeMirror from '@uiw/react-codemirror'; +import { vim } from "@replit/codemirror-vim"; +import { defaultKeymap } from "@codemirror/commands"; +import { javascript } from "@codemirror/lang-javascript"; +import { Scrollbar } from "@radix-ui/react-scroll-area"; + +interface CodePreviewProps { + code: string; + filepath: string; + keymapType: "default" | "vim"; + onClose: () => void; +} + +export const CodePreview = ({ + code, + filepath, + keymapType, + onClose, +}: CodePreviewProps) => { + 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); + } + }) + ]} + /> + + +
+ ) +} \ No newline at end of file diff --git a/src/app/page.tsx b/src/app/page.tsx index e933c8fc..cd69d001 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,7 +1,5 @@ 'use client'; -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; import { ResizableHandle, ResizablePanel, @@ -9,56 +7,19 @@ import { } from "@/components/ui/resizable"; import { Separator } from "@/components/ui/separator"; import { useNonEmptyQueryParam } from "@/hooks/useNonEmptyQueryParam"; -import { GetSourceResponse, pathQueryParamName, repoQueryParamName } from "@/lib/api"; +import { GetSourceResponse, pathQueryParamName, repoQueryParamName, ZoektFileMatch } from "@/lib/types"; import { createPathWithQueryParams } from "@/lib/utils"; -import { defaultKeymap } from "@codemirror/commands"; -import { javascript } from "@codemirror/lang-javascript"; -import { EditorView, keymap, ViewPlugin, ViewUpdate } from "@codemirror/view"; -import { Cross1Icon, FileIcon, SymbolIcon } from "@radix-ui/react-icons"; -import { ScrollArea, Scrollbar } from "@radix-ui/react-scroll-area"; -import { vim } from "@replit/codemirror-vim"; -import CodeMirror from '@uiw/react-codemirror'; -import { useTheme } from "next-themes"; +import { SymbolIcon } from "@radix-ui/react-icons"; import Image from "next/image"; import { useRouter } from "next/navigation"; -import { useEffect, useMemo, useState } from "react"; -import { useDebouncedCallback } from 'use-debounce'; +import { useState } from "react"; import logoDark from "../../public/sb_logo_dark.png"; import logoLight from "../../public/sb_logo_light.png"; +import { CodePreview } from "./codePreview"; +import { SearchBar } from "./searchBar"; +import { SearchResults } from "./searchResults"; import { ThemeSelectorButton } from "./themeSelectorButton"; -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") ?? ""; @@ -71,7 +32,7 @@ export default function Home() { const [code, setCode] = useState(""); const [filepath, setFilepath] = useState(""); - const [fileMatches, setFileMatches] = useState([]); + const [fileMatches, setFileMatches] = useState([]); const [isLoading, setIsLoading] = useState(false); const [searchDurationMs, setSearchDurationMs] = useState(0); @@ -80,6 +41,7 @@ export default function Home() { return (
+ {/* TopBar */}
@@ -119,228 +81,44 @@ export default function Home() {
+ + {/* Search Results & Code Preview */} - -
- {fileMatches.map((match, index) => ( - { - const url = createPathWithQueryParams( - `http://localhost:3000/api/source`, - [pathQueryParamName, match.FileName], - [repoQueryParamName, match.Repo] - ); + { + 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); - }); - }} - /> - ))} -
- -
+ // @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)} - keymapType="default" - /> - - )}
); } - -interface CodeEditorProps { - code: string; - filepath: string; - keymapType: "default" | "vim"; - onClose: () => void; -} - -const CodeEditor = ({ - code, - filepath, - keymapType, - 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}

- -
- ); - })} -
- ); -} diff --git a/src/app/searchBar.tsx b/src/app/searchBar.tsx new file mode 100644 index 00000000..e1acb43d --- /dev/null +++ b/src/app/searchBar.tsx @@ -0,0 +1,63 @@ +'use client'; + +import { Input } from "@/components/ui/input"; +import { ZoektResult, ZoektSearchResponse } from "@/lib/types"; +import { useEffect } from "react"; +import { useDebouncedCallback } from "use-debounce"; + +interface SearchBarProps { + query: string; + numResults: number; + onLoadingChange: (isLoading: boolean) => void; + onQueryChange: (query: string) => void; + onSearchResult: (result?: ZoektResult) => void, +} + +export 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: ZoektSearchResponse }) => { + 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); + }} + /> + ) +} \ No newline at end of file diff --git a/src/app/searchResults.tsx b/src/app/searchResults.tsx new file mode 100644 index 00000000..2c67cb87 --- /dev/null +++ b/src/app/searchResults.tsx @@ -0,0 +1,68 @@ +'use client'; + +import { ScrollArea } from "@/components/ui/scroll-area"; +import { Separator } from "@/components/ui/separator"; +import { ZoektFileMatch } from "@/lib/types"; +import { Scrollbar } from "@radix-ui/react-scroll-area"; + +interface SearchResultsProps { + fileMatches: ZoektFileMatch[]; + onOpenFileMatch: (match: ZoektFileMatch) => void; +} + +export const SearchResults = ({ + fileMatches, + onOpenFileMatch, +}: SearchResultsProps) => { + return ( + +
+ {fileMatches.map((match, index) => ( + { + onOpenFileMatch(match); + }} + /> + ))} +
+ +
+ ) +} + +interface FileMatchProps { + match: ZoektFileMatch; + 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}

+ +
+ ); + })} +
+ ); +} diff --git a/src/lib/api.ts b/src/lib/api.ts deleted file mode 100644 index a2855732..00000000 --- a/src/lib/api.ts +++ /dev/null @@ -1,8 +0,0 @@ - - -export const pathQueryParamName = "path"; -export const repoQueryParamName = "repo"; - -export type GetSourceResponse = { - content: string; -} diff --git a/src/lib/types.ts b/src/lib/types.ts new file mode 100644 index 00000000..2a8e2430 --- /dev/null +++ b/src/lib/types.ts @@ -0,0 +1,40 @@ + + +export const pathQueryParamName = "path"; +export const repoQueryParamName = "repo"; + +export type GetSourceResponse = { + content: string; +} + +export interface ZoektMatch { + URL: string, + FileName: string, + LineNum: number, + Fragments: { + Pre: string, + Match: string, + Post: string + }[] +} + +export interface ZoektFileMatch { + FileName: string, + Repo: string, + Language: string, + Matches: ZoektMatch[], + URL: string, +} + +export interface ZoektResult { + QueryStr: string, + FileMatches: ZoektFileMatch[] | null, + Stats: { + // Duration in nanoseconds + Duration: number, + } +} + +export interface ZoektSearchResponse { + result: ZoektResult, +} \ No newline at end of file