From 0a4db0a44b2ae7e4bd39d165691ae6b3fa1ff647 Mon Sep 17 00:00:00 2001 From: bkellam Date: Wed, 25 Sep 2024 16:07:52 -0700 Subject: [PATCH 1/6] wip on adding hotkey support for navigation --- src/app/globals.css | 10 +++ src/app/search/searchResultsPanel.tsx | 91 ++++++++++++++++++--------- 2 files changed, 71 insertions(+), 30 deletions(-) diff --git a/src/app/globals.css b/src/app/globals.css index 99a7b0c0a..688661942 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -66,4 +66,14 @@ body { @apply bg-background text-foreground; } +} + +.cm-editor .cm-gutters { + background-color: transparent; + border-right: none; +} + +.cm-editor .cm-lineNumbers .cm-gutterElement { + padding-left: 0.5; + text-align: left; } \ No newline at end of file diff --git a/src/app/search/searchResultsPanel.tsx b/src/app/search/searchResultsPanel.tsx index 469f58c7f..904c21191 100644 --- a/src/app/search/searchResultsPanel.tsx +++ b/src/app/search/searchResultsPanel.tsx @@ -13,7 +13,7 @@ import { Scrollbar } from "@radix-ui/react-scroll-area"; import CodeMirror, { Decoration, DecorationSet, EditorState, EditorView, ReactCodeMirrorRef, StateField, Transaction } from '@uiw/react-codemirror'; import clsx from "clsx"; import Image from "next/image"; -import { useMemo, useRef, useState } from "react"; +import { useCallback, useMemo, useRef, useState } from "react"; const MAX_MATCHES_TO_PREVIEW = 3; @@ -38,9 +38,11 @@ export const SearchResultsPanel = ({ } return ( - + {fileMatches.map((fileMatch, index) => ( - { @@ -56,17 +58,17 @@ export const SearchResultsPanel = ({ ) } -interface FilePreviewProps { +interface FileMatchContainerProps { file: SearchResultFile; onOpenFile: () => void; onMatchIndexChanged: (matchIndex: number) => void; } -const FilePreview = ({ +const FileMatchContainer = ({ file, onOpenFile, onMatchIndexChanged, -}: FilePreviewProps) => { +}: FileMatchContainerProps) => { const [showAll, setShowAll] = useState(false); const matchCount = useMemo(() => { @@ -124,6 +126,19 @@ const FilePreview = ({ return matchCount > MAX_MATCHES_TO_PREVIEW; }, [matchCount]); + const onShowMoreMatches = useCallback(() => { + setShowAll(!showAll); + }, [showAll]); + + const onOpenMatch = useCallback((index: number) => { + const matchIndex = matches.slice(0, index).reduce((acc, match) => { + return acc + match.Ranges.length; + }, 0); + onOpenFile(); + onMatchIndexChanged(matchIndex); + }, [matches, onMatchIndexChanged, onOpenFile]); + + return (
ยท {!fileNameRange ? ( - {file.FileName} + {file.FileName} ) : ( {file.FileName.slice(0, fileNameRange.from)} @@ -173,21 +188,26 @@ const FilePreview = ({ return (
{ - const matchIndex = matches.slice(0, index).reduce((acc, match) => { - return acc + match.Ranges.length; - }, 0); - onOpenFile(); - onMatchIndexChanged(matchIndex); - }} > - +
{ + if (e.key !== "Enter") { + return; + } + onOpenMatch(index); + }} + onClick={() => onOpenMatch(index)} + > + +
+ {(index !== matches.length - 1 || isMoreContentButtonVisible) && ( )} @@ -195,9 +215,18 @@ const FilePreview = ({ ); })} {isMoreContentButtonVisible && ( -
+
{ + if (e.key !== "Enter") { + return; + } + onShowMoreMatches(); + }} + onClick={onShowMoreMatches} + >

setShowAll(!showAll)} className="text-blue-500 cursor-pointer text-sm flex flex-row items-center gap-2" > {showAll ? : } @@ -222,17 +251,19 @@ const cmTheme = EditorView.baseTheme({ }, }); +interface CodePreviewProps { + content: string, + language: string, + ranges: SearchResultRange[], + lineOffset: number, +} + const CodePreview = ({ content, language, ranges, lineOffset, -}: { - content: string, - language: string, - ranges: SearchResultRange[], - lineOffset: number, -}) => { +}: CodePreviewProps) => { const editorRef = useRef(null); const { theme } = useThemeNormalized(); @@ -251,7 +282,7 @@ const CodePreview = ({ .filter(({ Start, End }) => { const startLine = Start.LineNumber - lineOffset; const endLine = End.LineNumber - lineOffset; - + if ( startLine < 1 || endLine < 1 || From d6866e8b15d6e5f30c02c804ff7e04e6bd9ff734 Mon Sep 17 00:00:00 2001 From: bkellam Date: Wed, 25 Sep 2024 16:26:35 -0700 Subject: [PATCH 2/6] refactor componenets into seperate files --- .../codePreviewPanel/codePreview.tsx} | 6 +- .../components/codePreviewPanel/index.tsx | 63 ++++++ .../searchResultsPanel/codePreview.tsx | 134 +++++++++++++ .../fileMatchContainer.tsx} | 186 +----------------- .../components/searchResultsPanel/index.tsx | 47 +++++ src/app/search/page.tsx | 66 +------ 6 files changed, 259 insertions(+), 243 deletions(-) rename src/app/search/{codePreviewPanel.tsx => components/codePreviewPanel/codePreview.tsx} (98%) create mode 100644 src/app/search/components/codePreviewPanel/index.tsx create mode 100644 src/app/search/components/searchResultsPanel/codePreview.tsx rename src/app/search/{searchResultsPanel.tsx => components/searchResultsPanel/fileMatchContainer.tsx} (51%) create mode 100644 src/app/search/components/searchResultsPanel/index.tsx diff --git a/src/app/search/codePreviewPanel.tsx b/src/app/search/components/codePreviewPanel/codePreview.tsx similarity index 98% rename from src/app/search/codePreviewPanel.tsx rename to src/app/search/components/codePreviewPanel/codePreview.tsx index 32aaaef17..a58f47b6c 100644 --- a/src/app/search/codePreviewPanel.tsx +++ b/src/app/search/components/codePreviewPanel/codePreview.tsx @@ -28,19 +28,19 @@ export interface CodePreviewFile { language: string; } -interface CodePreviewPanelProps { +interface CodePreviewProps { file?: CodePreviewFile; selectedMatchIndex: number; onSelectedMatchIndexChange: (index: number) => void; onClose: () => void; } -export const CodePreviewPanel = ({ +export const CodePreview = ({ file, selectedMatchIndex, onSelectedMatchIndexChange, onClose, -}: CodePreviewPanelProps) => { +}: CodePreviewProps) => { const editorRef = useRef(null); const [ keymapType ] = useKeymapType(); diff --git a/src/app/search/components/codePreviewPanel/index.tsx b/src/app/search/components/codePreviewPanel/index.tsx new file mode 100644 index 000000000..74cb0b897 --- /dev/null +++ b/src/app/search/components/codePreviewPanel/index.tsx @@ -0,0 +1,63 @@ +'use client'; + +import { fetchFileSource } from "@/app/api/(client)/client"; +import { SearchResultFile } from "@/lib/schemas"; +import { getCodeHostFilePreviewLink } from "@/lib/utils"; +import { useQuery } from "@tanstack/react-query"; +import { CodePreview, CodePreviewFile } from "./codePreview"; + +interface CodePreviewPanelProps { + fileMatch?: SearchResultFile; + onClose: () => void; + selectedMatchIndex: number; + onSelectedMatchIndexChange: (index: number) => void; +} + +export const CodePreviewPanel = ({ + fileMatch, + onClose, + selectedMatchIndex, + onSelectedMatchIndexChange, +}: CodePreviewPanelProps) => { + + const { data: file } = useQuery({ + queryKey: ["source", fileMatch?.FileName, fileMatch?.Repository], + queryFn: async (): Promise => { + if (!fileMatch) { + return undefined; + } + + return fetchFileSource(fileMatch.FileName, fileMatch.Repository) + .then(({ source }) => { + // @todo : refector this to use the templates provided by zoekt. + const link = getCodeHostFilePreviewLink(fileMatch.Repository, fileMatch.FileName) + + const decodedSource = atob(source); + + // Filter out filename matches + const filteredMatches = fileMatch.ChunkMatches.filter((match) => { + return !match.FileName; + }); + + return { + content: decodedSource, + filepath: fileMatch.FileName, + matches: filteredMatches, + link: link, + language: fileMatch.Language, + }; + }); + }, + enabled: fileMatch !== undefined, + }); + + return ( + + ) + +} \ No newline at end of file diff --git a/src/app/search/components/searchResultsPanel/codePreview.tsx b/src/app/search/components/searchResultsPanel/codePreview.tsx new file mode 100644 index 000000000..6dd69b304 --- /dev/null +++ b/src/app/search/components/searchResultsPanel/codePreview.tsx @@ -0,0 +1,134 @@ +'use client'; + +import { useExtensionWithDependency } from "@/hooks/useExtensionWithDependency"; +import { useSyntaxHighlightingExtension } from "@/hooks/useSyntaxHighlightingExtension"; +import { useThemeNormalized } from "@/hooks/useThemeNormalized"; +import { lineOffsetExtension } from "@/lib/extensions/lineOffsetExtension"; +import { SearchResultRange } from "@/lib/schemas"; +import CodeMirror, { Decoration, DecorationSet, EditorState, EditorView, ReactCodeMirrorRef, StateField, Transaction } from "@uiw/react-codemirror"; +import { useMemo, useRef } from "react"; + +const markDecoration = Decoration.mark({ + class: "cm-searchMatch" +}); + +const cmTheme = EditorView.baseTheme({ + "&light .cm-searchMatch": { + border: "1px #6b7280ff", + }, + "&dark .cm-searchMatch": { + border: "1px #d1d5dbff", + }, +}); + +interface CodePreviewProps { + content: string, + language: string, + ranges: SearchResultRange[], + lineOffset: number, +} + +export const CodePreview = ({ + content, + language, + ranges, + lineOffset, +}: CodePreviewProps) => { + const editorRef = useRef(null); + const { theme } = useThemeNormalized(); + + const syntaxHighlighting = useSyntaxHighlightingExtension(language, editorRef.current?.view); + + const rangeHighlighting = useExtensionWithDependency(editorRef.current?.view ?? null, () => { + return [ + StateField.define({ + create(editorState: EditorState) { + const document = editorState.doc; + + const decorations = ranges + .sort((a, b) => { + return a.Start.ByteOffset - b.Start.ByteOffset; + }) + .filter(({ Start, End }) => { + const startLine = Start.LineNumber - lineOffset; + const endLine = End.LineNumber - lineOffset; + + if ( + startLine < 1 || + endLine < 1 || + startLine > document.lines || + endLine > document.lines + ) { + return false; + } + return true; + }) + .map(({ Start, End }) => { + const startLine = Start.LineNumber - lineOffset; + const endLine = End.LineNumber - lineOffset; + + const from = document.line(startLine).from + Start.Column - 1; + const to = document.line(endLine).from + End.Column - 1; + return markDecoration.range(from, to); + }); + + return Decoration.set(decorations); + }, + update(highlights: DecorationSet, _transaction: Transaction) { + return highlights; + }, + provide: (field) => EditorView.decorations.from(field), + }), + cmTheme + ]; + }, [ranges, lineOffset]); + + const extensions = useMemo(() => { + return [ + syntaxHighlighting, + lineOffsetExtension(lineOffset), + rangeHighlighting, + ]; + }, [syntaxHighlighting, lineOffset, rangeHighlighting]); + + return ( + + ) +} \ No newline at end of file diff --git a/src/app/search/searchResultsPanel.tsx b/src/app/search/components/searchResultsPanel/fileMatchContainer.tsx similarity index 51% rename from src/app/search/searchResultsPanel.tsx rename to src/app/search/components/searchResultsPanel/fileMatchContainer.tsx index 904c21191..1ef2342b4 100644 --- a/src/app/search/searchResultsPanel.tsx +++ b/src/app/search/components/searchResultsPanel/fileMatchContainer.tsx @@ -1,70 +1,23 @@ 'use client'; -import { ScrollArea } from "@/components/ui/scroll-area"; -import { Separator } from "@/components/ui/separator"; -import { useExtensionWithDependency } from "@/hooks/useExtensionWithDependency"; -import { useSyntaxHighlightingExtension } from "@/hooks/useSyntaxHighlightingExtension"; -import { useThemeNormalized } from "@/hooks/useThemeNormalized"; -import { lineOffsetExtension } from "@/lib/extensions/lineOffsetExtension"; -import { SearchResultFile, SearchResultRange } from "@/lib/schemas"; +import { SearchResultFile } from "@/lib/schemas"; import { getRepoCodeHostInfo } from "@/lib/utils"; +import { useCallback, useMemo, useState } from "react"; +import Image from "next/image"; import { DoubleArrowDownIcon, DoubleArrowUpIcon, FileIcon } from "@radix-ui/react-icons"; -import { Scrollbar } from "@radix-ui/react-scroll-area"; -import CodeMirror, { Decoration, DecorationSet, EditorState, EditorView, ReactCodeMirrorRef, StateField, Transaction } from '@uiw/react-codemirror'; import clsx from "clsx"; -import Image from "next/image"; -import { useCallback, useMemo, useRef, useState } from "react"; +import { Separator } from "@/components/ui/separator"; +import { CodePreview } from "./codePreview"; const MAX_MATCHES_TO_PREVIEW = 3; -interface SearchResultsPanelProps { - fileMatches: SearchResultFile[]; - onOpenFileMatch: (fileMatch: SearchResultFile) => void; - onMatchIndexChanged: (matchIndex: number) => void; -} - -export const SearchResultsPanel = ({ - fileMatches, - onOpenFileMatch, - onMatchIndexChanged, -}: SearchResultsPanelProps) => { - - if (fileMatches.length === 0) { - return ( -

-

No results found

-
- ); - } - - return ( - - {fileMatches.map((fileMatch, index) => ( - { - onOpenFileMatch(fileMatch); - }} - onMatchIndexChanged={(matchIndex) => { - onMatchIndexChanged(matchIndex); - }} - /> - ))} - - - ) -} - interface FileMatchContainerProps { file: SearchResultFile; onOpenFile: () => void; onMatchIndexChanged: (matchIndex: number) => void; } -const FileMatchContainer = ({ +export const FileMatchContainer = ({ file, onOpenFile, onMatchIndexChanged, @@ -236,129 +189,4 @@ const FileMatchContainer = ({ )}
); -} - -const markDecoration = Decoration.mark({ - class: "cm-searchMatch" -}); - -const cmTheme = EditorView.baseTheme({ - "&light .cm-searchMatch": { - border: "1px #6b7280ff", - }, - "&dark .cm-searchMatch": { - border: "1px #d1d5dbff", - }, -}); - -interface CodePreviewProps { - content: string, - language: string, - ranges: SearchResultRange[], - lineOffset: number, -} - -const CodePreview = ({ - content, - language, - ranges, - lineOffset, -}: CodePreviewProps) => { - const editorRef = useRef(null); - const { theme } = useThemeNormalized(); - - const syntaxHighlighting = useSyntaxHighlightingExtension(language, editorRef.current?.view); - - const rangeHighlighting = useExtensionWithDependency(editorRef.current?.view ?? null, () => { - return [ - StateField.define({ - create(editorState: EditorState) { - const document = editorState.doc; - - const decorations = ranges - .sort((a, b) => { - return a.Start.ByteOffset - b.Start.ByteOffset; - }) - .filter(({ Start, End }) => { - const startLine = Start.LineNumber - lineOffset; - const endLine = End.LineNumber - lineOffset; - - if ( - startLine < 1 || - endLine < 1 || - startLine > document.lines || - endLine > document.lines - ) { - return false; - } - return true; - }) - .map(({ Start, End }) => { - const startLine = Start.LineNumber - lineOffset; - const endLine = End.LineNumber - lineOffset; - - const from = document.line(startLine).from + Start.Column - 1; - const to = document.line(endLine).from + End.Column - 1; - return markDecoration.range(from, to); - }); - - return Decoration.set(decorations); - }, - update(highlights: DecorationSet, _transaction: Transaction) { - return highlights; - }, - provide: (field) => EditorView.decorations.from(field), - }), - cmTheme - ]; - }, [ranges, lineOffset]); - - const extensions = useMemo(() => { - return [ - syntaxHighlighting, - lineOffsetExtension(lineOffset), - rangeHighlighting, - ]; - }, [syntaxHighlighting, lineOffset, rangeHighlighting]); - - return ( - - ) -} +} \ No newline at end of file diff --git a/src/app/search/components/searchResultsPanel/index.tsx b/src/app/search/components/searchResultsPanel/index.tsx new file mode 100644 index 000000000..27a6194fc --- /dev/null +++ b/src/app/search/components/searchResultsPanel/index.tsx @@ -0,0 +1,47 @@ +'use client'; + +import { ScrollArea } from "@/components/ui/scroll-area"; +import { SearchResultFile } from "@/lib/schemas"; +import { Scrollbar } from "@radix-ui/react-scroll-area"; +import { FileMatchContainer } from "./fileMatchContainer"; + +interface SearchResultsPanelProps { + fileMatches: SearchResultFile[]; + onOpenFileMatch: (fileMatch: SearchResultFile) => void; + onMatchIndexChanged: (matchIndex: number) => void; +} + +export const SearchResultsPanel = ({ + fileMatches, + onOpenFileMatch, + onMatchIndexChanged, +}: SearchResultsPanelProps) => { + + if (fileMatches.length === 0) { + return ( +
+

No results found

+
+ ); + } + + 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 b5616d3cc..9fa9c14e3 100644 --- a/src/app/search/page.tsx +++ b/src/app/search/page.tsx @@ -8,7 +8,7 @@ import { import { Separator } from "@/components/ui/separator"; import { useNonEmptyQueryParam } from "@/hooks/useNonEmptyQueryParam"; import { SearchResultFile } from "@/lib/schemas"; -import { createPathWithQueryParams, getCodeHostFilePreviewLink } from "@/lib/utils"; +import { createPathWithQueryParams } from "@/lib/utils"; import { SymbolIcon } from "@radix-ui/react-icons"; import { useQuery } from "@tanstack/react-query"; import Image from "next/image"; @@ -16,12 +16,12 @@ import { useRouter } from "next/navigation"; import { useEffect, useMemo, useState } from "react"; import logoDark from "../../../public/sb_logo_dark.png"; import logoLight from "../../../public/sb_logo_light.png"; -import { fetchFileSource, search } from "../api/(client)/client"; +import { search } from "../api/(client)/client"; import { SearchBar } from "../searchBar"; import { SettingsDropdown } from "../settingsDropdown"; -import { CodePreviewFile, CodePreviewPanel } from "./codePreviewPanel"; -import { SearchResultsPanel } from "./searchResultsPanel"; import useCaptureEvent from "@/hooks/useCaptureEvent"; +import { CodePreviewPanel } from "./components/codePreviewPanel"; +import { SearchResultsPanel } from "./components/searchResultsPanel"; const DEFAULT_NUM_RESULTS = 100; @@ -174,7 +174,7 @@ export default function SearchPage() { minSize={20} hidden={!selectedFile} > - setSelectedFile(undefined)} selectedMatchIndex={selectedMatchIndex} @@ -185,59 +185,3 @@ export default function SearchPage() {
); } - -interface CodePreviewWrapperProps { - fileMatch?: SearchResultFile; - onClose: () => void; - selectedMatchIndex: number; - onSelectedMatchIndexChange: (index: number) => void; -} - -const CodePreviewWrapper = ({ - fileMatch, - onClose, - selectedMatchIndex, - onSelectedMatchIndexChange, -}: CodePreviewWrapperProps) => { - - const { data: file } = useQuery({ - queryKey: ["source", fileMatch?.FileName, fileMatch?.Repository], - queryFn: async (): Promise => { - if (!fileMatch) { - return undefined; - } - - return fetchFileSource(fileMatch.FileName, fileMatch.Repository) - .then(({ source }) => { - // @todo : refector this to use the templates provided by zoekt. - const link = getCodeHostFilePreviewLink(fileMatch.Repository, fileMatch.FileName) - - const decodedSource = atob(source); - - // Filter out filename matches - const filteredMatches = fileMatch.ChunkMatches.filter((match) => { - return !match.FileName; - }); - - return { - content: decodedSource, - filepath: fileMatch.FileName, - matches: filteredMatches, - link: link, - language: fileMatch.Language, - }; - }); - }, - enabled: fileMatch !== undefined, - }); - - return ( - - ) - -} \ No newline at end of file From 422a6a9b1253607aa1f6673c9b01a39050315e30 Mon Sep 17 00:00:00 2001 From: bkellam Date: Wed, 25 Sep 2024 17:04:13 -0700 Subject: [PATCH 3/6] move schema types to types.ts --- src/app/api/(client)/client.ts | 3 ++- src/app/repositoryCarousel.tsx | 2 +- .../codePreviewPanel/codePreview.tsx | 2 +- .../components/codePreviewPanel/index.tsx | 2 +- .../searchResultsPanel/codePreview.tsx | 2 +- .../searchResultsPanel/fileMatchContainer.tsx | 2 +- .../components/searchResultsPanel/index.tsx | 2 +- src/app/search/page.tsx | 2 +- .../searchResultHighlightExtension.ts | 2 +- src/lib/schemas.ts | 20 +++---------------- src/lib/server/searchService.ts | 3 ++- src/lib/types.ts | 20 ++++++++++++++++++- 12 files changed, 34 insertions(+), 28 deletions(-) diff --git a/src/app/api/(client)/client.ts b/src/app/api/(client)/client.ts index 92bf8e53d..965102f5b 100644 --- a/src/app/api/(client)/client.ts +++ b/src/app/api/(client)/client.ts @@ -1,4 +1,5 @@ -import { FileSourceResponse, fileSourceResponseSchema, ListRepositoriesResponse, listRepositoriesResponseSchema, SearchRequest, SearchResponse, searchResponseSchema } from "@/lib/schemas"; +import { fileSourceResponseSchema, listRepositoriesResponseSchema, searchResponseSchema } from "@/lib/schemas"; +import { FileSourceResponse, ListRepositoriesResponse, SearchRequest, SearchResponse } from "@/lib/types"; export const search = async (body: SearchRequest): Promise => { const result = await fetch(`/api/search`, { diff --git a/src/app/repositoryCarousel.tsx b/src/app/repositoryCarousel.tsx index aa498686e..7b5dcc1d9 100644 --- a/src/app/repositoryCarousel.tsx +++ b/src/app/repositoryCarousel.tsx @@ -1,6 +1,5 @@ 'use client'; -import { Repository } from "@/lib/schemas"; import { Carousel, CarouselContent, @@ -11,6 +10,7 @@ import { getRepoCodeHostInfo } from "@/lib/utils"; import Image from "next/image"; import { FileIcon } from "@radix-ui/react-icons"; import clsx from "clsx"; +import { Repository } from "@/lib/types"; interface RepositoryCarouselProps { repos: Repository[]; diff --git a/src/app/search/components/codePreviewPanel/codePreview.tsx b/src/app/search/components/codePreviewPanel/codePreview.tsx index a58f47b6c..8a9c36ddd 100644 --- a/src/app/search/components/codePreviewPanel/codePreview.tsx +++ b/src/app/search/components/codePreviewPanel/codePreview.tsx @@ -8,7 +8,7 @@ import { useSyntaxHighlightingExtension } from "@/hooks/useSyntaxHighlightingExt import { useThemeNormalized } from "@/hooks/useThemeNormalized"; import { gutterWidthExtension } from "@/lib/extensions/gutterWidthExtension"; import { highlightRanges, searchResultHighlightExtension } from "@/lib/extensions/searchResultHighlightExtension"; -import { SearchResultFileMatch } from "@/lib/schemas"; +import { SearchResultFileMatch } from "@/lib/types"; import { defaultKeymap } from "@codemirror/commands"; import { search } from "@codemirror/search"; import { EditorView, keymap } from "@codemirror/view"; diff --git a/src/app/search/components/codePreviewPanel/index.tsx b/src/app/search/components/codePreviewPanel/index.tsx index 74cb0b897..3cc88cc9c 100644 --- a/src/app/search/components/codePreviewPanel/index.tsx +++ b/src/app/search/components/codePreviewPanel/index.tsx @@ -1,10 +1,10 @@ 'use client'; import { fetchFileSource } from "@/app/api/(client)/client"; -import { SearchResultFile } from "@/lib/schemas"; import { getCodeHostFilePreviewLink } from "@/lib/utils"; import { useQuery } from "@tanstack/react-query"; import { CodePreview, CodePreviewFile } from "./codePreview"; +import { SearchResultFile } from "@/lib/types"; interface CodePreviewPanelProps { fileMatch?: SearchResultFile; diff --git a/src/app/search/components/searchResultsPanel/codePreview.tsx b/src/app/search/components/searchResultsPanel/codePreview.tsx index 6dd69b304..f958848e9 100644 --- a/src/app/search/components/searchResultsPanel/codePreview.tsx +++ b/src/app/search/components/searchResultsPanel/codePreview.tsx @@ -4,7 +4,7 @@ import { useExtensionWithDependency } from "@/hooks/useExtensionWithDependency"; import { useSyntaxHighlightingExtension } from "@/hooks/useSyntaxHighlightingExtension"; import { useThemeNormalized } from "@/hooks/useThemeNormalized"; import { lineOffsetExtension } from "@/lib/extensions/lineOffsetExtension"; -import { SearchResultRange } from "@/lib/schemas"; +import { SearchResultRange } from "@/lib/types"; import CodeMirror, { Decoration, DecorationSet, EditorState, EditorView, ReactCodeMirrorRef, StateField, Transaction } from "@uiw/react-codemirror"; import { useMemo, useRef } from "react"; diff --git a/src/app/search/components/searchResultsPanel/fileMatchContainer.tsx b/src/app/search/components/searchResultsPanel/fileMatchContainer.tsx index 1ef2342b4..37173575d 100644 --- a/src/app/search/components/searchResultsPanel/fileMatchContainer.tsx +++ b/src/app/search/components/searchResultsPanel/fileMatchContainer.tsx @@ -1,6 +1,5 @@ 'use client'; -import { SearchResultFile } from "@/lib/schemas"; import { getRepoCodeHostInfo } from "@/lib/utils"; import { useCallback, useMemo, useState } from "react"; import Image from "next/image"; @@ -8,6 +7,7 @@ import { DoubleArrowDownIcon, DoubleArrowUpIcon, FileIcon } from "@radix-ui/reac import clsx from "clsx"; import { Separator } from "@/components/ui/separator"; import { CodePreview } from "./codePreview"; +import { SearchResultFile } from "@/lib/types"; const MAX_MATCHES_TO_PREVIEW = 3; diff --git a/src/app/search/components/searchResultsPanel/index.tsx b/src/app/search/components/searchResultsPanel/index.tsx index 27a6194fc..023f6bf35 100644 --- a/src/app/search/components/searchResultsPanel/index.tsx +++ b/src/app/search/components/searchResultsPanel/index.tsx @@ -1,9 +1,9 @@ 'use client'; import { ScrollArea } from "@/components/ui/scroll-area"; -import { SearchResultFile } from "@/lib/schemas"; import { Scrollbar } from "@radix-ui/react-scroll-area"; import { FileMatchContainer } from "./fileMatchContainer"; +import { SearchResultFile } from "@/lib/types"; interface SearchResultsPanelProps { fileMatches: SearchResultFile[]; diff --git a/src/app/search/page.tsx b/src/app/search/page.tsx index 9fa9c14e3..0690f8c19 100644 --- a/src/app/search/page.tsx +++ b/src/app/search/page.tsx @@ -7,7 +7,6 @@ import { } from "@/components/ui/resizable"; import { Separator } from "@/components/ui/separator"; import { useNonEmptyQueryParam } from "@/hooks/useNonEmptyQueryParam"; -import { SearchResultFile } from "@/lib/schemas"; import { createPathWithQueryParams } from "@/lib/utils"; import { SymbolIcon } from "@radix-ui/react-icons"; import { useQuery } from "@tanstack/react-query"; @@ -22,6 +21,7 @@ import { SettingsDropdown } from "../settingsDropdown"; import useCaptureEvent from "@/hooks/useCaptureEvent"; import { CodePreviewPanel } from "./components/codePreviewPanel"; import { SearchResultsPanel } from "./components/searchResultsPanel"; +import { SearchResultFile } from "@/lib/types"; const DEFAULT_NUM_RESULTS = 100; diff --git a/src/lib/extensions/searchResultHighlightExtension.ts b/src/lib/extensions/searchResultHighlightExtension.ts index 223d02853..968b28d82 100644 --- a/src/lib/extensions/searchResultHighlightExtension.ts +++ b/src/lib/extensions/searchResultHighlightExtension.ts @@ -1,6 +1,6 @@ import { EditorSelection, Extension, StateEffect, StateField, Text, Transaction } from "@codemirror/state"; import { Decoration, DecorationSet, EditorView } from "@codemirror/view"; -import { SearchResultRange } from "../schemas"; +import { SearchResultRange } from "../types"; const setMatchState = StateEffect.define<{ selectedMatchIndex: number, diff --git a/src/lib/schemas.ts b/src/lib/schemas.ts index a3b35b423..3c012bff2 100644 --- a/src/lib/schemas.ts +++ b/src/lib/schemas.ts @@ -1,6 +1,5 @@ import { z } from "zod"; -export type SearchRequest = z.infer; export const searchRequestSchema = z.object({ query: z.string(), numResults: z.number(), @@ -8,15 +7,8 @@ export const searchRequestSchema = z.object({ }); -export type SearchResponse = z.infer; -export type SearchResult = SearchResponse["Result"]; -export type SearchResultFile = NonNullable[number]; -export type SearchResultFileMatch = SearchResultFile["ChunkMatches"][number]; -export type SearchResultRange = z.infer; -export type SearchResultLocation = z.infer; - // @see : https://github.com/TaqlaAI/zoekt/blob/main/api.go#L212 -const locationSchema = z.object({ +export const locationSchema = z.object({ // 0-based byte offset from the beginning of the file ByteOffset: z.number(), // 1-based line number from the beginning of the file @@ -25,7 +17,7 @@ const locationSchema = z.object({ Column: z.number(), }); -const rangeSchema = z.object({ +export const rangeSchema = z.object({ Start: locationSchema, End: locationSchema, }); @@ -79,22 +71,16 @@ export const searchResponseSchema = z.object({ }), }); -export type FileSourceRequest = z.infer; export const fileSourceRequestSchema = z.object({ fileName: z.string(), repository: z.string() }); -export type FileSourceResponse = z.infer; - export const fileSourceResponseSchema = z.object({ source: z.string(), }); -export type ListRepositoriesResponse = z.infer; -export type Repository = z.infer; - // @see : https://github.com/TaqlaAI/zoekt/blob/3780e68cdb537d5a7ed2c84d9b3784f80c7c5d04/api.go#L728 const repoStatsSchema = z.object({ Repos: z.number(), @@ -120,7 +106,7 @@ const indexMetadataSchema = z.object({ }); // @see : https://github.com/TaqlaAI/zoekt/blob/3780e68cdb537d5a7ed2c84d9b3784f80c7c5d04/api.go#L555 -const repositorySchema = z.object({ +export const repositorySchema = z.object({ Name: z.string(), URL: z.string(), Source: z.string(), diff --git a/src/lib/server/searchService.ts b/src/lib/server/searchService.ts index 0d537be8a..2bc57ea13 100644 --- a/src/lib/server/searchService.ts +++ b/src/lib/server/searchService.ts @@ -1,6 +1,7 @@ import escapeStringRegexp from "escape-string-regexp"; import { SHARD_MAX_MATCH_COUNT, TOTAL_MAX_MATCH_COUNT } from "../environment"; -import { FileSourceRequest, FileSourceResponse, ListRepositoriesResponse, listRepositoriesResponseSchema, SearchRequest, SearchResponse, searchResponseSchema } from "../schemas"; +import { listRepositoriesResponseSchema, searchResponseSchema } from "../schemas"; +import { FileSourceRequest, FileSourceResponse, ListRepositoriesResponse, SearchRequest, SearchResponse } from "../types"; import { fileNotFound, invalidZoektResponse, ServiceError, unexpectedError } from "../serviceError"; import { isServiceError } from "../utils"; import { zoektFetch } from "./zoektClient"; diff --git a/src/lib/types.ts b/src/lib/types.ts index 4d0d1045e..e9cfdbd9c 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -1 +1,19 @@ -export type KeymapType = "default" | "vim"; \ No newline at end of file +import { z } from "zod"; +import { fileSourceRequestSchema, fileSourceResponseSchema, listRepositoriesResponseSchema, locationSchema, rangeSchema, repositorySchema, searchRequestSchema, searchResponseSchema } from "./schemas"; + +export type KeymapType = "default" | "vim"; + +export type SearchResponse = z.infer; +export type SearchResult = SearchResponse["Result"]; +export type SearchResultFile = NonNullable[number]; +export type SearchResultFileMatch = SearchResultFile["ChunkMatches"][number]; +export type SearchResultRange = z.infer; +export type SearchResultLocation = z.infer; + +export type FileSourceRequest = z.infer; +export type FileSourceResponse = z.infer; + +export type ListRepositoriesResponse = z.infer; +export type Repository = z.infer; +export type SearchRequest = z.infer; + From a695f0d667673dbd5d6445d46ae2a9463b760a97 Mon Sep 17 00:00:00 2001 From: bkellam Date: Wed, 25 Sep 2024 17:46:07 -0700 Subject: [PATCH 4/6] linewrap code editors --- src/app/search/components/codePreviewPanel/codePreview.tsx | 1 + src/app/search/components/searchResultsPanel/codePreview.tsx | 1 + 2 files changed, 2 insertions(+) diff --git a/src/app/search/components/codePreviewPanel/codePreview.tsx b/src/app/search/components/codePreviewPanel/codePreview.tsx index 8a9c36ddd..35f764dae 100644 --- a/src/app/search/components/codePreviewPanel/codePreview.tsx +++ b/src/app/search/components/codePreviewPanel/codePreview.tsx @@ -67,6 +67,7 @@ export const CodePreview = ({ keymapExtension, gutterWidthExtension, syntaxHighlighting, + EditorView.lineWrapping, searchResultHighlightExtension(), search({ top: true, diff --git a/src/app/search/components/searchResultsPanel/codePreview.tsx b/src/app/search/components/searchResultsPanel/codePreview.tsx index f958848e9..3216bb512 100644 --- a/src/app/search/components/searchResultsPanel/codePreview.tsx +++ b/src/app/search/components/searchResultsPanel/codePreview.tsx @@ -86,6 +86,7 @@ export const CodePreview = ({ const extensions = useMemo(() => { return [ syntaxHighlighting, + EditorView.lineWrapping, lineOffsetExtension(lineOffset), rangeHighlighting, ]; From 6ddb917a718bfbbdecca16316c53453cda19ee4d Mon Sep 17 00:00:00 2001 From: bkellam Date: Wed, 25 Sep 2024 17:46:51 -0700 Subject: [PATCH 5/6] refactor into fileMatch component --- .../searchResultsPanel/fileMatch.tsx | 48 ++++++++++++++++ .../searchResultsPanel/fileMatchContainer.tsx | 57 ++++++------------- 2 files changed, 65 insertions(+), 40 deletions(-) create mode 100644 src/app/search/components/searchResultsPanel/fileMatch.tsx diff --git a/src/app/search/components/searchResultsPanel/fileMatch.tsx b/src/app/search/components/searchResultsPanel/fileMatch.tsx new file mode 100644 index 000000000..da0778787 --- /dev/null +++ b/src/app/search/components/searchResultsPanel/fileMatch.tsx @@ -0,0 +1,48 @@ +'use client'; + +import { useMemo } from "react"; +import { CodePreview } from "./codePreview"; +import { SearchResultFile, SearchResultFileMatch } from "@/lib/types"; + + +interface FileMatchProps { + match: SearchResultFileMatch; + file: SearchResultFile; + onOpen: () => void; +} + +export const FileMatch = ({ + match, + file, + onOpen, +}: FileMatchProps) => { + const content = useMemo(() => { + return atob(match.Content); + }, [match.Content]); + + // If it's just the title, don't show a code preview + if (match.FileName) { + return null; + } + + return ( +
{ + if (e.key !== "Enter") { + return; + } + onOpen(); + }} + onClick={onOpen} + > + +
+ ); +} \ No newline at end of file diff --git a/src/app/search/components/searchResultsPanel/fileMatchContainer.tsx b/src/app/search/components/searchResultsPanel/fileMatchContainer.tsx index 37173575d..e2c3084b7 100644 --- a/src/app/search/components/searchResultsPanel/fileMatchContainer.tsx +++ b/src/app/search/components/searchResultsPanel/fileMatchContainer.tsx @@ -6,8 +6,8 @@ import Image from "next/image"; import { DoubleArrowDownIcon, DoubleArrowUpIcon, FileIcon } from "@radix-ui/react-icons"; import clsx from "clsx"; import { Separator } from "@/components/ui/separator"; -import { CodePreview } from "./codePreview"; import { SearchResultFile } from "@/lib/types"; +import { FileMatch } from "./fileMatch"; const MAX_MATCHES_TO_PREVIEW = 3; @@ -128,45 +128,22 @@ export const FileMatchContainer = ({ )}
- {matches.map((match, index) => { - const content = atob(match.Content); - - // If it's just the title, don't show a code preview - if (match.FileName) { - return null; - } - - const lineOffset = match.ContentStart.LineNumber - 1; - - return ( -
-
{ - if (e.key !== "Enter") { - return; - } - onOpenMatch(index); - }} - onClick={() => onOpenMatch(index)} - > - -
- - {(index !== matches.length - 1 || isMoreContentButtonVisible) && ( - - )} -
- ); - })} + {matches.map((match, index) => ( +
+ { + onOpenMatch(index); + }} + /> + {(index !== matches.length - 1 || isMoreContentButtonVisible) && ( + + )} +
+ ))} {isMoreContentButtonVisible && (
Date: Wed, 25 Sep 2024 18:11:16 -0700 Subject: [PATCH 6/6] Improve search result highlighting --- src/app/globals.css | 9 ++++++++ .../searchResultsPanel/codePreview.tsx | 12 +---------- .../searchResultHighlightExtension.ts | 21 ++----------------- 3 files changed, 12 insertions(+), 30 deletions(-) diff --git a/src/app/globals.css b/src/app/globals.css index 688661942..7153c1ee7 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -76,4 +76,13 @@ .cm-editor .cm-lineNumbers .cm-gutterElement { padding-left: 0.5; text-align: left; +} + +.cm-editor .cm-searchMatch { + border: dotted; + background: transparent; +} + +.cm-editor .cm-searchMatch-selected { + border: solid; } \ No newline at end of file diff --git a/src/app/search/components/searchResultsPanel/codePreview.tsx b/src/app/search/components/searchResultsPanel/codePreview.tsx index 3216bb512..e7ebb7ed3 100644 --- a/src/app/search/components/searchResultsPanel/codePreview.tsx +++ b/src/app/search/components/searchResultsPanel/codePreview.tsx @@ -9,16 +9,7 @@ import CodeMirror, { Decoration, DecorationSet, EditorState, EditorView, ReactCo import { useMemo, useRef } from "react"; const markDecoration = Decoration.mark({ - class: "cm-searchMatch" -}); - -const cmTheme = EditorView.baseTheme({ - "&light .cm-searchMatch": { - border: "1px #6b7280ff", - }, - "&dark .cm-searchMatch": { - border: "1px #d1d5dbff", - }, + class: "cm-searchMatch-selected" }); interface CodePreviewProps { @@ -79,7 +70,6 @@ export const CodePreview = ({ }, provide: (field) => EditorView.decorations.from(field), }), - cmTheme ]; }, [ranges, lineOffset]); diff --git a/src/lib/extensions/searchResultHighlightExtension.ts b/src/lib/extensions/searchResultHighlightExtension.ts index 968b28d82..de2844f81 100644 --- a/src/lib/extensions/searchResultHighlightExtension.ts +++ b/src/lib/extensions/searchResultHighlightExtension.ts @@ -46,26 +46,10 @@ const matchHighlighter = StateField.define({ }); const matchMark = Decoration.mark({ - class: "tq-searchMatch" + class: "cm-searchMatch" }); const selectedMatchMark = Decoration.mark({ - class: "tq-searchMatch-selected" -}); - -const highlightTheme = EditorView.baseTheme({ - "&light .tq-searchMatch": { - border: "1px dotted #6b7280ff", - }, - "&light .tq-searchMatch-selected": { - backgroundColor: "#00ff00aa" - }, - - "&dark .tq-searchMatch": { - border: "1px dotted #d1d5dbff", - }, - "&dark .tq-searchMatch-selected": { - backgroundColor: "#00ff007a", - } + class: "cm-searchMatch-selected" }); export const highlightRanges = (selectedMatchIndex: number, ranges: SearchResultRange[], view: EditorView) => { @@ -90,7 +74,6 @@ export const highlightRanges = (selectedMatchIndex: number, ranges: SearchResult export const searchResultHighlightExtension = (): Extension => { return [ - highlightTheme, matchHighlighter, ] }