From afede30de66ef8fbb753e0ba40950fd27a26a628 Mon Sep 17 00:00:00 2001 From: Brendan Kellam Date: Wed, 25 Sep 2024 20:12:20 -0700 Subject: [PATCH] Hotkeys, cleanup, and UX improvements (#9) --- src/app/api/(client)/client.ts | 3 +- src/app/globals.css | 19 + src/app/repositoryCarousel.tsx | 2 +- .../codePreviewPanel/codePreview.tsx} | 9 +- .../components/codePreviewPanel/index.tsx | 63 ++++ .../searchResultsPanel/codePreview.tsx | 125 +++++++ .../searchResultsPanel/fileMatch.tsx | 48 +++ .../searchResultsPanel/fileMatchContainer.tsx | 169 +++++++++ .../components/searchResultsPanel/index.tsx | 47 +++ src/app/search/page.tsx | 68 +--- src/app/search/searchResultsPanel.tsx | 333 ------------------ .../searchResultHighlightExtension.ts | 23 +- src/lib/schemas.ts | 20 +- src/lib/server/searchService.ts | 3 +- src/lib/types.ts | 20 +- 15 files changed, 512 insertions(+), 440 deletions(-) rename src/app/search/{codePreviewPanel.tsx => components/codePreviewPanel/codePreview.tsx} (97%) create mode 100644 src/app/search/components/codePreviewPanel/index.tsx create mode 100644 src/app/search/components/searchResultsPanel/codePreview.tsx create mode 100644 src/app/search/components/searchResultsPanel/fileMatch.tsx create mode 100644 src/app/search/components/searchResultsPanel/fileMatchContainer.tsx create mode 100644 src/app/search/components/searchResultsPanel/index.tsx delete mode 100644 src/app/search/searchResultsPanel.tsx diff --git a/src/app/api/(client)/client.ts b/src/app/api/(client)/client.ts index 92bf8e53..965102f5 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/globals.css b/src/app/globals.css index 99a7b0c0..7153c1ee 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -66,4 +66,23 @@ 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; +} + +.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/repositoryCarousel.tsx b/src/app/repositoryCarousel.tsx index aa498686..7b5dcc1d 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/codePreviewPanel.tsx b/src/app/search/components/codePreviewPanel/codePreview.tsx similarity index 97% rename from src/app/search/codePreviewPanel.tsx rename to src/app/search/components/codePreviewPanel/codePreview.tsx index 32aaaef1..35f764da 100644 --- a/src/app/search/codePreviewPanel.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"; @@ -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(); @@ -67,6 +67,7 @@ export const CodePreviewPanel = ({ keymapExtension, gutterWidthExtension, syntaxHighlighting, + EditorView.lineWrapping, searchResultHighlightExtension(), search({ top: true, diff --git a/src/app/search/components/codePreviewPanel/index.tsx b/src/app/search/components/codePreviewPanel/index.tsx new file mode 100644 index 00000000..3cc88cc9 --- /dev/null +++ b/src/app/search/components/codePreviewPanel/index.tsx @@ -0,0 +1,63 @@ +'use client'; + +import { fetchFileSource } from "@/app/api/(client)/client"; +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; + 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 00000000..e7ebb7ed --- /dev/null +++ b/src/app/search/components/searchResultsPanel/codePreview.tsx @@ -0,0 +1,125 @@ +'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/types"; +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-selected" +}); + +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), + }), + ]; + }, [ranges, lineOffset]); + + const extensions = useMemo(() => { + return [ + syntaxHighlighting, + EditorView.lineWrapping, + lineOffsetExtension(lineOffset), + rangeHighlighting, + ]; + }, [syntaxHighlighting, lineOffset, rangeHighlighting]); + + return ( + + ) +} \ No newline at end of file diff --git a/src/app/search/components/searchResultsPanel/fileMatch.tsx b/src/app/search/components/searchResultsPanel/fileMatch.tsx new file mode 100644 index 00000000..da077878 --- /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 new file mode 100644 index 00000000..e2c3084b --- /dev/null +++ b/src/app/search/components/searchResultsPanel/fileMatchContainer.tsx @@ -0,0 +1,169 @@ +'use client'; + +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 clsx from "clsx"; +import { Separator } from "@/components/ui/separator"; +import { SearchResultFile } from "@/lib/types"; +import { FileMatch } from "./fileMatch"; + +const MAX_MATCHES_TO_PREVIEW = 3; + +interface FileMatchContainerProps { + file: SearchResultFile; + onOpenFile: () => void; + onMatchIndexChanged: (matchIndex: number) => void; +} + +export const FileMatchContainer = ({ + file, + onOpenFile, + onMatchIndexChanged, +}: FileMatchContainerProps) => { + + const [showAll, setShowAll] = useState(false); + const matchCount = useMemo(() => { + return file.ChunkMatches.length; + }, [file]); + + const matches = useMemo(() => { + const sortedMatches = file.ChunkMatches.sort((a, b) => { + return a.ContentStart.LineNumber - b.ContentStart.LineNumber; + }); + + if (!showAll) { + return sortedMatches.slice(0, MAX_MATCHES_TO_PREVIEW); + } + + return sortedMatches; + }, [file, showAll]); + + const fileNameRange = useMemo(() => { + for (const match of matches) { + if (match.FileName && match.Ranges.length > 0) { + const range = match.Ranges[0]; + return { + from: range.Start.Column - 1, + to: range.End.Column - 1, + } + } + } + + return null; + }, [matches]); + + const { repoIcon, repoName, repoLink } = useMemo(() => { + const info = getRepoCodeHostInfo(file.Repository); + if (info) { + return { + repoName: info.repoName, + repoLink: info.repoLink, + repoIcon: {info.costHostName} + } + } + + return { + repoName: file.Repository, + repoLink: undefined, + repoIcon: + } + }, [file]); + + const isMoreContentButtonVisible = useMemo(() => { + 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 ( +
+
{ + onOpenFile(); + }} + > +
+ {repoIcon} + { + if (repoLink) { + window.open(repoLink, "_blank"); + } + }} + > + {repoName} + + · + {!fileNameRange ? ( + {file.FileName} + ) : ( + + {file.FileName.slice(0, fileNameRange.from)} + + {file.FileName.slice(fileNameRange.from, fileNameRange.to)} + + {file.FileName.slice(fileNameRange.to)} + + )} +
+
+ {matches.map((match, index) => ( +
+ { + onOpenMatch(index); + }} + /> + {(index !== matches.length - 1 || isMoreContentButtonVisible) && ( + + )} +
+ ))} + {isMoreContentButtonVisible && ( +
{ + if (e.key !== "Enter") { + return; + } + onShowMoreMatches(); + }} + onClick={onShowMoreMatches} + > +

+ {showAll ? : } + {showAll ? `Show fewer matches` : `Show ${matchCount - MAX_MATCHES_TO_PREVIEW} more matches`} +

+
+ )} +
+ ); +} \ 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 00000000..023f6bf3 --- /dev/null +++ b/src/app/search/components/searchResultsPanel/index.tsx @@ -0,0 +1,47 @@ +'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"; + +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 b5616d3c..0690f8c1 100644 --- a/src/app/search/page.tsx +++ b/src/app/search/page.tsx @@ -7,8 +7,7 @@ import { } from "@/components/ui/resizable"; 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 +15,13 @@ 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"; +import { SearchResultFile } from "@/lib/types"; 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 diff --git a/src/app/search/searchResultsPanel.tsx b/src/app/search/searchResultsPanel.tsx deleted file mode 100644 index 469f58c7..00000000 --- a/src/app/search/searchResultsPanel.tsx +++ /dev/null @@ -1,333 +0,0 @@ -'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 { getRepoCodeHostInfo } from "@/lib/utils"; -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 { useMemo, useRef, useState } from "react"; - -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 FilePreviewProps { - file: SearchResultFile; - onOpenFile: () => void; - onMatchIndexChanged: (matchIndex: number) => void; -} - -const FilePreview = ({ - file, - onOpenFile, - onMatchIndexChanged, -}: FilePreviewProps) => { - - const [showAll, setShowAll] = useState(false); - const matchCount = useMemo(() => { - return file.ChunkMatches.length; - }, [file]); - - const matches = useMemo(() => { - const sortedMatches = file.ChunkMatches.sort((a, b) => { - return a.ContentStart.LineNumber - b.ContentStart.LineNumber; - }); - - if (!showAll) { - return sortedMatches.slice(0, MAX_MATCHES_TO_PREVIEW); - } - - return sortedMatches; - }, [file, showAll]); - - const fileNameRange = useMemo(() => { - for (const match of matches) { - if (match.FileName && match.Ranges.length > 0) { - const range = match.Ranges[0]; - return { - from: range.Start.Column - 1, - to: range.End.Column - 1, - } - } - } - - return null; - }, [matches]); - - const { repoIcon, repoName, repoLink } = useMemo(() => { - const info = getRepoCodeHostInfo(file.Repository); - if (info) { - return { - repoName: info.repoName, - repoLink: info.repoLink, - repoIcon: {info.costHostName} - } - } - - return { - repoName: file.Repository, - repoLink: undefined, - repoIcon: - } - }, [file]); - - const isMoreContentButtonVisible = useMemo(() => { - return matchCount > MAX_MATCHES_TO_PREVIEW; - }, [matchCount]); - - return ( -
-
{ - onOpenFile(); - }} - > -
- {repoIcon} - { - if (repoLink) { - window.open(repoLink, "_blank"); - } - }} - > - {repoName} - - · - {!fileNameRange ? ( - {file.FileName} - ) : ( - - {file.FileName.slice(0, fileNameRange.from)} - - {file.FileName.slice(fileNameRange.from, fileNameRange.to)} - - {file.FileName.slice(fileNameRange.to)} - - )} -
-
- {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 ( -
{ - const matchIndex = matches.slice(0, index).reduce((acc, match) => { - return acc + match.Ranges.length; - }, 0); - onOpenFile(); - onMatchIndexChanged(matchIndex); - }} - > - - {(index !== matches.length - 1 || isMoreContentButtonVisible) && ( - - )} -
- ); - })} - {isMoreContentButtonVisible && ( -
-

setShowAll(!showAll)} - className="text-blue-500 cursor-pointer text-sm flex flex-row items-center gap-2" - > - {showAll ? : } - {showAll ? `Show fewer matches` : `Show ${matchCount - MAX_MATCHES_TO_PREVIEW} more matches`} -

-
- )} -
- ); -} - -const markDecoration = Decoration.mark({ - class: "cm-searchMatch" -}); - -const cmTheme = EditorView.baseTheme({ - "&light .cm-searchMatch": { - border: "1px #6b7280ff", - }, - "&dark .cm-searchMatch": { - border: "1px #d1d5dbff", - }, -}); - -const CodePreview = ({ - content, - language, - ranges, - lineOffset, -}: { - content: string, - language: string, - ranges: SearchResultRange[], - lineOffset: number, -}) => { - 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 ( - - ) -} diff --git a/src/lib/extensions/searchResultHighlightExtension.ts b/src/lib/extensions/searchResultHighlightExtension.ts index 223d0285..de2844f8 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, @@ -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, ] } diff --git a/src/lib/schemas.ts b/src/lib/schemas.ts index a3b35b42..3c012bff 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 0d537be8..2bc57ea1 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 4d0d1045..e9cfdbd9 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; +