mirror of
https://github.com/sourcebot-dev/sourcebot.git
synced 2025-12-14 13:25:21 +00:00
Hotkeys, cleanup, and UX improvements (#9)
This commit is contained in:
parent
a51a5e2764
commit
afede30de6
15 changed files with 512 additions and 440 deletions
|
|
@ -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<SearchResponse> => {
|
export const search = async (body: SearchRequest): Promise<SearchResponse> => {
|
||||||
const result = await fetch(`/api/search`, {
|
const result = await fetch(`/api/search`, {
|
||||||
|
|
|
||||||
|
|
@ -66,4 +66,23 @@
|
||||||
body {
|
body {
|
||||||
@apply bg-background text-foreground;
|
@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;
|
||||||
}
|
}
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Repository } from "@/lib/schemas";
|
|
||||||
import {
|
import {
|
||||||
Carousel,
|
Carousel,
|
||||||
CarouselContent,
|
CarouselContent,
|
||||||
|
|
@ -11,6 +10,7 @@ import { getRepoCodeHostInfo } from "@/lib/utils";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { FileIcon } from "@radix-ui/react-icons";
|
import { FileIcon } from "@radix-ui/react-icons";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
|
import { Repository } from "@/lib/types";
|
||||||
|
|
||||||
interface RepositoryCarouselProps {
|
interface RepositoryCarouselProps {
|
||||||
repos: Repository[];
|
repos: Repository[];
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import { useSyntaxHighlightingExtension } from "@/hooks/useSyntaxHighlightingExt
|
||||||
import { useThemeNormalized } from "@/hooks/useThemeNormalized";
|
import { useThemeNormalized } from "@/hooks/useThemeNormalized";
|
||||||
import { gutterWidthExtension } from "@/lib/extensions/gutterWidthExtension";
|
import { gutterWidthExtension } from "@/lib/extensions/gutterWidthExtension";
|
||||||
import { highlightRanges, searchResultHighlightExtension } from "@/lib/extensions/searchResultHighlightExtension";
|
import { highlightRanges, searchResultHighlightExtension } from "@/lib/extensions/searchResultHighlightExtension";
|
||||||
import { SearchResultFileMatch } from "@/lib/schemas";
|
import { SearchResultFileMatch } from "@/lib/types";
|
||||||
import { defaultKeymap } from "@codemirror/commands";
|
import { defaultKeymap } from "@codemirror/commands";
|
||||||
import { search } from "@codemirror/search";
|
import { search } from "@codemirror/search";
|
||||||
import { EditorView, keymap } from "@codemirror/view";
|
import { EditorView, keymap } from "@codemirror/view";
|
||||||
|
|
@ -28,19 +28,19 @@ export interface CodePreviewFile {
|
||||||
language: string;
|
language: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CodePreviewPanelProps {
|
interface CodePreviewProps {
|
||||||
file?: CodePreviewFile;
|
file?: CodePreviewFile;
|
||||||
selectedMatchIndex: number;
|
selectedMatchIndex: number;
|
||||||
onSelectedMatchIndexChange: (index: number) => void;
|
onSelectedMatchIndexChange: (index: number) => void;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CodePreviewPanel = ({
|
export const CodePreview = ({
|
||||||
file,
|
file,
|
||||||
selectedMatchIndex,
|
selectedMatchIndex,
|
||||||
onSelectedMatchIndexChange,
|
onSelectedMatchIndexChange,
|
||||||
onClose,
|
onClose,
|
||||||
}: CodePreviewPanelProps) => {
|
}: CodePreviewProps) => {
|
||||||
const editorRef = useRef<ReactCodeMirrorRef>(null);
|
const editorRef = useRef<ReactCodeMirrorRef>(null);
|
||||||
|
|
||||||
const [ keymapType ] = useKeymapType();
|
const [ keymapType ] = useKeymapType();
|
||||||
|
|
@ -67,6 +67,7 @@ export const CodePreviewPanel = ({
|
||||||
keymapExtension,
|
keymapExtension,
|
||||||
gutterWidthExtension,
|
gutterWidthExtension,
|
||||||
syntaxHighlighting,
|
syntaxHighlighting,
|
||||||
|
EditorView.lineWrapping,
|
||||||
searchResultHighlightExtension(),
|
searchResultHighlightExtension(),
|
||||||
search({
|
search({
|
||||||
top: true,
|
top: true,
|
||||||
63
src/app/search/components/codePreviewPanel/index.tsx
Normal file
63
src/app/search/components/codePreviewPanel/index.tsx
Normal file
|
|
@ -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<CodePreviewFile | undefined> => {
|
||||||
|
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 (
|
||||||
|
<CodePreview
|
||||||
|
file={file}
|
||||||
|
onClose={onClose}
|
||||||
|
selectedMatchIndex={selectedMatchIndex}
|
||||||
|
onSelectedMatchIndexChange={onSelectedMatchIndexChange}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
125
src/app/search/components/searchResultsPanel/codePreview.tsx
Normal file
125
src/app/search/components/searchResultsPanel/codePreview.tsx
Normal file
|
|
@ -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<ReactCodeMirrorRef>(null);
|
||||||
|
const { theme } = useThemeNormalized();
|
||||||
|
|
||||||
|
const syntaxHighlighting = useSyntaxHighlightingExtension(language, editorRef.current?.view);
|
||||||
|
|
||||||
|
const rangeHighlighting = useExtensionWithDependency(editorRef.current?.view ?? null, () => {
|
||||||
|
return [
|
||||||
|
StateField.define<DecorationSet>({
|
||||||
|
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 (
|
||||||
|
<CodeMirror
|
||||||
|
ref={editorRef}
|
||||||
|
readOnly={true}
|
||||||
|
editable={false}
|
||||||
|
value={content}
|
||||||
|
theme={theme === "dark" ? "dark" : "light"}
|
||||||
|
basicSetup={{
|
||||||
|
lineNumbers: true,
|
||||||
|
syntaxHighlighting: true,
|
||||||
|
|
||||||
|
// Disable all this other stuff...
|
||||||
|
... {
|
||||||
|
foldGutter: false,
|
||||||
|
highlightActiveLineGutter: false,
|
||||||
|
highlightSpecialChars: false,
|
||||||
|
history: false,
|
||||||
|
drawSelection: false,
|
||||||
|
dropCursor: false,
|
||||||
|
allowMultipleSelections: false,
|
||||||
|
indentOnInput: false,
|
||||||
|
bracketMatching: false,
|
||||||
|
closeBrackets: false,
|
||||||
|
autocompletion: false,
|
||||||
|
rectangularSelection: false,
|
||||||
|
crosshairCursor: false,
|
||||||
|
highlightActiveLine: false,
|
||||||
|
highlightSelectionMatches: false,
|
||||||
|
closeBracketsKeymap: false,
|
||||||
|
defaultKeymap: false,
|
||||||
|
searchKeymap: false,
|
||||||
|
historyKeymap: false,
|
||||||
|
foldKeymap: false,
|
||||||
|
completionKeymap: false,
|
||||||
|
lintKeymap: false,
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
extensions={extensions}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
48
src/app/search/components/searchResultsPanel/fileMatch.tsx
Normal file
48
src/app/search/components/searchResultsPanel/fileMatch.tsx
Normal file
|
|
@ -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 (
|
||||||
|
<div
|
||||||
|
tabIndex={0}
|
||||||
|
className="cursor-pointer p-1 focus:ring-inset focus:ring-4 bg-white dark:bg-[#282c34]"
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key !== "Enter") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onOpen();
|
||||||
|
}}
|
||||||
|
onClick={onOpen}
|
||||||
|
>
|
||||||
|
<CodePreview
|
||||||
|
content={content}
|
||||||
|
language={file.Language}
|
||||||
|
ranges={match.Ranges}
|
||||||
|
lineOffset={match.ContentStart.LineNumber - 1}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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: <Image
|
||||||
|
src={info.icon}
|
||||||
|
alt={info.costHostName}
|
||||||
|
className="w-4 h-4 dark:invert"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
repoName: file.Repository,
|
||||||
|
repoLink: undefined,
|
||||||
|
repoIcon: <FileIcon className="w-4 h-4" />
|
||||||
|
}
|
||||||
|
}, [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 (
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
className="sticky top-0 bg-cyan-200 dark:bg-cyan-900 primary-foreground px-2 py-0.5 flex flex-row items-center justify-between border cursor-pointer z-10"
|
||||||
|
onClick={() => {
|
||||||
|
onOpenFile();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex flex-row gap-2 items-center">
|
||||||
|
{repoIcon}
|
||||||
|
<span
|
||||||
|
className={clsx("font-medium", {
|
||||||
|
"cursor-pointer hover:underline": repoLink,
|
||||||
|
})}
|
||||||
|
onClick={() => {
|
||||||
|
if (repoLink) {
|
||||||
|
window.open(repoLink, "_blank");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{repoName}
|
||||||
|
</span>
|
||||||
|
<span>·</span>
|
||||||
|
{!fileNameRange ? (
|
||||||
|
<span>{file.FileName}</span>
|
||||||
|
) : (
|
||||||
|
<span>
|
||||||
|
{file.FileName.slice(0, fileNameRange.from)}
|
||||||
|
<span className="bg-yellow-200 dark:bg-blue-700">
|
||||||
|
{file.FileName.slice(fileNameRange.from, fileNameRange.to)}
|
||||||
|
</span>
|
||||||
|
{file.FileName.slice(fileNameRange.to)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{matches.map((match, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
>
|
||||||
|
<FileMatch
|
||||||
|
match={match}
|
||||||
|
file={file}
|
||||||
|
onOpen={() => {
|
||||||
|
onOpenMatch(index);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{(index !== matches.length - 1 || isMoreContentButtonVisible) && (
|
||||||
|
<Separator className="dark:bg-gray-400" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{isMoreContentButtonVisible && (
|
||||||
|
<div
|
||||||
|
tabIndex={0}
|
||||||
|
className="px-4 bg-accent p-0.5"
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key !== "Enter") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onShowMoreMatches();
|
||||||
|
}}
|
||||||
|
onClick={onShowMoreMatches}
|
||||||
|
>
|
||||||
|
<p
|
||||||
|
className="text-blue-500 cursor-pointer text-sm flex flex-row items-center gap-2"
|
||||||
|
>
|
||||||
|
{showAll ? <DoubleArrowUpIcon className="w-3 h-3" /> : <DoubleArrowDownIcon className="w-3 h-3" />}
|
||||||
|
{showAll ? `Show fewer matches` : `Show ${matchCount - MAX_MATCHES_TO_PREVIEW} more matches`}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
47
src/app/search/components/searchResultsPanel/index.tsx
Normal file
47
src/app/search/components/searchResultsPanel/index.tsx
Normal file
|
|
@ -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 (
|
||||||
|
<div className="flex flex-col items-center justify-center h-full">
|
||||||
|
<p className="text-sm text-muted-foreground">No results found</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ScrollArea
|
||||||
|
className="h-full"
|
||||||
|
>
|
||||||
|
{fileMatches.map((fileMatch, index) => (
|
||||||
|
<FileMatchContainer
|
||||||
|
key={index}
|
||||||
|
file={fileMatch}
|
||||||
|
onOpenFile={() => {
|
||||||
|
onOpenFileMatch(fileMatch);
|
||||||
|
}}
|
||||||
|
onMatchIndexChanged={(matchIndex) => {
|
||||||
|
onMatchIndexChanged(matchIndex);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
<Scrollbar orientation="vertical" />
|
||||||
|
</ScrollArea>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -7,8 +7,7 @@ import {
|
||||||
} from "@/components/ui/resizable";
|
} from "@/components/ui/resizable";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { useNonEmptyQueryParam } from "@/hooks/useNonEmptyQueryParam";
|
import { useNonEmptyQueryParam } from "@/hooks/useNonEmptyQueryParam";
|
||||||
import { SearchResultFile } from "@/lib/schemas";
|
import { createPathWithQueryParams } from "@/lib/utils";
|
||||||
import { createPathWithQueryParams, getCodeHostFilePreviewLink } from "@/lib/utils";
|
|
||||||
import { SymbolIcon } from "@radix-ui/react-icons";
|
import { SymbolIcon } from "@radix-ui/react-icons";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
|
|
@ -16,12 +15,13 @@ import { useRouter } from "next/navigation";
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import logoDark from "../../../public/sb_logo_dark.png";
|
import logoDark from "../../../public/sb_logo_dark.png";
|
||||||
import logoLight from "../../../public/sb_logo_light.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 { SearchBar } from "../searchBar";
|
||||||
import { SettingsDropdown } from "../settingsDropdown";
|
import { SettingsDropdown } from "../settingsDropdown";
|
||||||
import { CodePreviewFile, CodePreviewPanel } from "./codePreviewPanel";
|
|
||||||
import { SearchResultsPanel } from "./searchResultsPanel";
|
|
||||||
import useCaptureEvent from "@/hooks/useCaptureEvent";
|
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;
|
const DEFAULT_NUM_RESULTS = 100;
|
||||||
|
|
||||||
|
|
@ -174,7 +174,7 @@ export default function SearchPage() {
|
||||||
minSize={20}
|
minSize={20}
|
||||||
hidden={!selectedFile}
|
hidden={!selectedFile}
|
||||||
>
|
>
|
||||||
<CodePreviewWrapper
|
<CodePreviewPanel
|
||||||
fileMatch={selectedFile}
|
fileMatch={selectedFile}
|
||||||
onClose={() => setSelectedFile(undefined)}
|
onClose={() => setSelectedFile(undefined)}
|
||||||
selectedMatchIndex={selectedMatchIndex}
|
selectedMatchIndex={selectedMatchIndex}
|
||||||
|
|
@ -185,59 +185,3 @@ export default function SearchPage() {
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
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<CodePreviewFile | undefined> => {
|
|
||||||
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 (
|
|
||||||
<CodePreviewPanel
|
|
||||||
file={file}
|
|
||||||
onClose={onClose}
|
|
||||||
selectedMatchIndex={selectedMatchIndex}
|
|
||||||
onSelectedMatchIndexChange={onSelectedMatchIndexChange}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -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 (
|
|
||||||
<div className="flex flex-col items-center justify-center h-full">
|
|
||||||
<p className="text-sm text-muted-foreground">No results found</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ScrollArea className="h-full">
|
|
||||||
{fileMatches.map((fileMatch, index) => (
|
|
||||||
<FilePreview
|
|
||||||
key={index}
|
|
||||||
file={fileMatch}
|
|
||||||
onOpenFile={() => {
|
|
||||||
onOpenFileMatch(fileMatch);
|
|
||||||
}}
|
|
||||||
onMatchIndexChanged={(matchIndex) => {
|
|
||||||
onMatchIndexChanged(matchIndex);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
<Scrollbar orientation="vertical" />
|
|
||||||
</ScrollArea>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
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: <Image
|
|
||||||
src={info.icon}
|
|
||||||
alt={info.costHostName}
|
|
||||||
className="w-4 h-4 dark:invert"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
repoName: file.Repository,
|
|
||||||
repoLink: undefined,
|
|
||||||
repoIcon: <FileIcon className="w-4 h-4" />
|
|
||||||
}
|
|
||||||
}, [file]);
|
|
||||||
|
|
||||||
const isMoreContentButtonVisible = useMemo(() => {
|
|
||||||
return matchCount > MAX_MATCHES_TO_PREVIEW;
|
|
||||||
}, [matchCount]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
className="sticky top-0 bg-cyan-200 dark:bg-cyan-900 primary-foreground px-2 py-0.5 flex flex-row items-center justify-between border cursor-pointer z-10"
|
|
||||||
onClick={() => {
|
|
||||||
onOpenFile();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="flex flex-row gap-2 items-center">
|
|
||||||
{repoIcon}
|
|
||||||
<span
|
|
||||||
className={clsx("font-medium", {
|
|
||||||
"cursor-pointer hover:underline": repoLink,
|
|
||||||
})}
|
|
||||||
onClick={() => {
|
|
||||||
if (repoLink) {
|
|
||||||
window.open(repoLink, "_blank");
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{repoName}
|
|
||||||
</span>
|
|
||||||
<span>·</span>
|
|
||||||
{!fileNameRange ? (
|
|
||||||
<span>{file.FileName}</span>
|
|
||||||
) : (
|
|
||||||
<span>
|
|
||||||
{file.FileName.slice(0, fileNameRange.from)}
|
|
||||||
<span className="bg-yellow-200 dark:bg-blue-700">
|
|
||||||
{file.FileName.slice(fileNameRange.from, fileNameRange.to)}
|
|
||||||
</span>
|
|
||||||
{file.FileName.slice(fileNameRange.to)}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{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 (
|
|
||||||
<div
|
|
||||||
key={index}
|
|
||||||
className="cursor-pointer"
|
|
||||||
onClick={() => {
|
|
||||||
const matchIndex = matches.slice(0, index).reduce((acc, match) => {
|
|
||||||
return acc + match.Ranges.length;
|
|
||||||
}, 0);
|
|
||||||
onOpenFile();
|
|
||||||
onMatchIndexChanged(matchIndex);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<CodePreview
|
|
||||||
content={content}
|
|
||||||
language={file.Language}
|
|
||||||
ranges={match.Ranges}
|
|
||||||
lineOffset={lineOffset}
|
|
||||||
/>
|
|
||||||
{(index !== matches.length - 1 || isMoreContentButtonVisible) && (
|
|
||||||
<Separator className="dark:bg-gray-400" />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
{isMoreContentButtonVisible && (
|
|
||||||
<div className="px-4 bg-accent">
|
|
||||||
<p
|
|
||||||
onClick={() => setShowAll(!showAll)}
|
|
||||||
className="text-blue-500 cursor-pointer text-sm flex flex-row items-center gap-2"
|
|
||||||
>
|
|
||||||
{showAll ? <DoubleArrowUpIcon className="w-3 h-3" /> : <DoubleArrowDownIcon className="w-3 h-3" />}
|
|
||||||
{showAll ? `Show fewer matches` : `Show ${matchCount - MAX_MATCHES_TO_PREVIEW} more matches`}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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<ReactCodeMirrorRef>(null);
|
|
||||||
const { theme } = useThemeNormalized();
|
|
||||||
|
|
||||||
const syntaxHighlighting = useSyntaxHighlightingExtension(language, editorRef.current?.view);
|
|
||||||
|
|
||||||
const rangeHighlighting = useExtensionWithDependency(editorRef.current?.view ?? null, () => {
|
|
||||||
return [
|
|
||||||
StateField.define<DecorationSet>({
|
|
||||||
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 (
|
|
||||||
<CodeMirror
|
|
||||||
ref={editorRef}
|
|
||||||
readOnly={true}
|
|
||||||
editable={false}
|
|
||||||
value={content}
|
|
||||||
theme={theme === "dark" ? "dark" : "light"}
|
|
||||||
basicSetup={{
|
|
||||||
lineNumbers: true,
|
|
||||||
syntaxHighlighting: true,
|
|
||||||
|
|
||||||
// Disable all this other stuff...
|
|
||||||
... {
|
|
||||||
foldGutter: false,
|
|
||||||
highlightActiveLineGutter: false,
|
|
||||||
highlightSpecialChars: false,
|
|
||||||
history: false,
|
|
||||||
drawSelection: false,
|
|
||||||
dropCursor: false,
|
|
||||||
allowMultipleSelections: false,
|
|
||||||
indentOnInput: false,
|
|
||||||
bracketMatching: false,
|
|
||||||
closeBrackets: false,
|
|
||||||
autocompletion: false,
|
|
||||||
rectangularSelection: false,
|
|
||||||
crosshairCursor: false,
|
|
||||||
highlightActiveLine: false,
|
|
||||||
highlightSelectionMatches: false,
|
|
||||||
closeBracketsKeymap: false,
|
|
||||||
defaultKeymap: false,
|
|
||||||
searchKeymap: false,
|
|
||||||
historyKeymap: false,
|
|
||||||
foldKeymap: false,
|
|
||||||
completionKeymap: false,
|
|
||||||
lintKeymap: false,
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
extensions={extensions}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { EditorSelection, Extension, StateEffect, StateField, Text, Transaction } from "@codemirror/state";
|
import { EditorSelection, Extension, StateEffect, StateField, Text, Transaction } from "@codemirror/state";
|
||||||
import { Decoration, DecorationSet, EditorView } from "@codemirror/view";
|
import { Decoration, DecorationSet, EditorView } from "@codemirror/view";
|
||||||
import { SearchResultRange } from "../schemas";
|
import { SearchResultRange } from "../types";
|
||||||
|
|
||||||
const setMatchState = StateEffect.define<{
|
const setMatchState = StateEffect.define<{
|
||||||
selectedMatchIndex: number,
|
selectedMatchIndex: number,
|
||||||
|
|
@ -46,26 +46,10 @@ const matchHighlighter = StateField.define<DecorationSet>({
|
||||||
});
|
});
|
||||||
|
|
||||||
const matchMark = Decoration.mark({
|
const matchMark = Decoration.mark({
|
||||||
class: "tq-searchMatch"
|
class: "cm-searchMatch"
|
||||||
});
|
});
|
||||||
const selectedMatchMark = Decoration.mark({
|
const selectedMatchMark = Decoration.mark({
|
||||||
class: "tq-searchMatch-selected"
|
class: "cm-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",
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const highlightRanges = (selectedMatchIndex: number, ranges: SearchResultRange[], view: EditorView) => {
|
export const highlightRanges = (selectedMatchIndex: number, ranges: SearchResultRange[], view: EditorView) => {
|
||||||
|
|
@ -90,7 +74,6 @@ export const highlightRanges = (selectedMatchIndex: number, ranges: SearchResult
|
||||||
|
|
||||||
export const searchResultHighlightExtension = (): Extension => {
|
export const searchResultHighlightExtension = (): Extension => {
|
||||||
return [
|
return [
|
||||||
highlightTheme,
|
|
||||||
matchHighlighter,
|
matchHighlighter,
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
export type SearchRequest = z.infer<typeof searchRequestSchema>;
|
|
||||||
export const searchRequestSchema = z.object({
|
export const searchRequestSchema = z.object({
|
||||||
query: z.string(),
|
query: z.string(),
|
||||||
numResults: z.number(),
|
numResults: z.number(),
|
||||||
|
|
@ -8,15 +7,8 @@ export const searchRequestSchema = z.object({
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
export type SearchResponse = z.infer<typeof searchResponseSchema>;
|
|
||||||
export type SearchResult = SearchResponse["Result"];
|
|
||||||
export type SearchResultFile = NonNullable<SearchResult["Files"]>[number];
|
|
||||||
export type SearchResultFileMatch = SearchResultFile["ChunkMatches"][number];
|
|
||||||
export type SearchResultRange = z.infer<typeof rangeSchema>;
|
|
||||||
export type SearchResultLocation = z.infer<typeof locationSchema>;
|
|
||||||
|
|
||||||
// @see : https://github.com/TaqlaAI/zoekt/blob/main/api.go#L212
|
// @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
|
// 0-based byte offset from the beginning of the file
|
||||||
ByteOffset: z.number(),
|
ByteOffset: z.number(),
|
||||||
// 1-based line number from the beginning of the file
|
// 1-based line number from the beginning of the file
|
||||||
|
|
@ -25,7 +17,7 @@ const locationSchema = z.object({
|
||||||
Column: z.number(),
|
Column: z.number(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const rangeSchema = z.object({
|
export const rangeSchema = z.object({
|
||||||
Start: locationSchema,
|
Start: locationSchema,
|
||||||
End: locationSchema,
|
End: locationSchema,
|
||||||
});
|
});
|
||||||
|
|
@ -79,22 +71,16 @@ export const searchResponseSchema = z.object({
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type FileSourceRequest = z.infer<typeof fileSourceRequestSchema>;
|
|
||||||
export const fileSourceRequestSchema = z.object({
|
export const fileSourceRequestSchema = z.object({
|
||||||
fileName: z.string(),
|
fileName: z.string(),
|
||||||
repository: z.string()
|
repository: z.string()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type FileSourceResponse = z.infer<typeof fileSourceResponseSchema>;
|
|
||||||
|
|
||||||
export const fileSourceResponseSchema = z.object({
|
export const fileSourceResponseSchema = z.object({
|
||||||
source: z.string(),
|
source: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
export type ListRepositoriesResponse = z.infer<typeof listRepositoriesResponseSchema>;
|
|
||||||
export type Repository = z.infer<typeof repositorySchema>;
|
|
||||||
|
|
||||||
// @see : https://github.com/TaqlaAI/zoekt/blob/3780e68cdb537d5a7ed2c84d9b3784f80c7c5d04/api.go#L728
|
// @see : https://github.com/TaqlaAI/zoekt/blob/3780e68cdb537d5a7ed2c84d9b3784f80c7c5d04/api.go#L728
|
||||||
const repoStatsSchema = z.object({
|
const repoStatsSchema = z.object({
|
||||||
Repos: z.number(),
|
Repos: z.number(),
|
||||||
|
|
@ -120,7 +106,7 @@ const indexMetadataSchema = z.object({
|
||||||
});
|
});
|
||||||
|
|
||||||
// @see : https://github.com/TaqlaAI/zoekt/blob/3780e68cdb537d5a7ed2c84d9b3784f80c7c5d04/api.go#L555
|
// @see : https://github.com/TaqlaAI/zoekt/blob/3780e68cdb537d5a7ed2c84d9b3784f80c7c5d04/api.go#L555
|
||||||
const repositorySchema = z.object({
|
export const repositorySchema = z.object({
|
||||||
Name: z.string(),
|
Name: z.string(),
|
||||||
URL: z.string(),
|
URL: z.string(),
|
||||||
Source: z.string(),
|
Source: z.string(),
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import escapeStringRegexp from "escape-string-regexp";
|
import escapeStringRegexp from "escape-string-regexp";
|
||||||
import { SHARD_MAX_MATCH_COUNT, TOTAL_MAX_MATCH_COUNT } from "../environment";
|
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 { fileNotFound, invalidZoektResponse, ServiceError, unexpectedError } from "../serviceError";
|
||||||
import { isServiceError } from "../utils";
|
import { isServiceError } from "../utils";
|
||||||
import { zoektFetch } from "./zoektClient";
|
import { zoektFetch } from "./zoektClient";
|
||||||
|
|
|
||||||
|
|
@ -1 +1,19 @@
|
||||||
export type KeymapType = "default" | "vim";
|
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<typeof searchResponseSchema>;
|
||||||
|
export type SearchResult = SearchResponse["Result"];
|
||||||
|
export type SearchResultFile = NonNullable<SearchResult["Files"]>[number];
|
||||||
|
export type SearchResultFileMatch = SearchResultFile["ChunkMatches"][number];
|
||||||
|
export type SearchResultRange = z.infer<typeof rangeSchema>;
|
||||||
|
export type SearchResultLocation = z.infer<typeof locationSchema>;
|
||||||
|
|
||||||
|
export type FileSourceRequest = z.infer<typeof fileSourceRequestSchema>;
|
||||||
|
export type FileSourceResponse = z.infer<typeof fileSourceResponseSchema>;
|
||||||
|
|
||||||
|
export type ListRepositoriesResponse = z.infer<typeof listRepositoriesResponseSchema>;
|
||||||
|
export type Repository = z.infer<typeof repositorySchema>;
|
||||||
|
export type SearchRequest = z.infer<typeof searchRequestSchema>;
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue