Skip to content

Commit afede30

Browse files
Hotkeys, cleanup, and UX improvements (#9)
1 parent a51a5e2 commit afede30

File tree

15 files changed

+512
-440
lines changed

15 files changed

+512
-440
lines changed

src/app/api/(client)/client.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { FileSourceResponse, fileSourceResponseSchema, ListRepositoriesResponse, listRepositoriesResponseSchema, SearchRequest, SearchResponse, searchResponseSchema } from "@/lib/schemas";
1+
import { fileSourceResponseSchema, listRepositoriesResponseSchema, searchResponseSchema } from "@/lib/schemas";
2+
import { FileSourceResponse, ListRepositoriesResponse, SearchRequest, SearchResponse } from "@/lib/types";
23

34
export const search = async (body: SearchRequest): Promise<SearchResponse> => {
45
const result = await fetch(`/api/search`, {

src/app/globals.css

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,4 +66,23 @@
6666
body {
6767
@apply bg-background text-foreground;
6868
}
69+
}
70+
71+
.cm-editor .cm-gutters {
72+
background-color: transparent;
73+
border-right: none;
74+
}
75+
76+
.cm-editor .cm-lineNumbers .cm-gutterElement {
77+
padding-left: 0.5;
78+
text-align: left;
79+
}
80+
81+
.cm-editor .cm-searchMatch {
82+
border: dotted;
83+
background: transparent;
84+
}
85+
86+
.cm-editor .cm-searchMatch-selected {
87+
border: solid;
6988
}

src/app/repositoryCarousel.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
'use client';
22

3-
import { Repository } from "@/lib/schemas";
43
import {
54
Carousel,
65
CarouselContent,
@@ -11,6 +10,7 @@ import { getRepoCodeHostInfo } from "@/lib/utils";
1110
import Image from "next/image";
1211
import { FileIcon } from "@radix-ui/react-icons";
1312
import clsx from "clsx";
13+
import { Repository } from "@/lib/types";
1414

1515
interface RepositoryCarouselProps {
1616
repos: Repository[];

src/app/search/codePreviewPanel.tsx renamed to src/app/search/components/codePreviewPanel/codePreview.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { useSyntaxHighlightingExtension } from "@/hooks/useSyntaxHighlightingExt
88
import { useThemeNormalized } from "@/hooks/useThemeNormalized";
99
import { gutterWidthExtension } from "@/lib/extensions/gutterWidthExtension";
1010
import { highlightRanges, searchResultHighlightExtension } from "@/lib/extensions/searchResultHighlightExtension";
11-
import { SearchResultFileMatch } from "@/lib/schemas";
11+
import { SearchResultFileMatch } from "@/lib/types";
1212
import { defaultKeymap } from "@codemirror/commands";
1313
import { search } from "@codemirror/search";
1414
import { EditorView, keymap } from "@codemirror/view";
@@ -28,19 +28,19 @@ export interface CodePreviewFile {
2828
language: string;
2929
}
3030

31-
interface CodePreviewPanelProps {
31+
interface CodePreviewProps {
3232
file?: CodePreviewFile;
3333
selectedMatchIndex: number;
3434
onSelectedMatchIndexChange: (index: number) => void;
3535
onClose: () => void;
3636
}
3737

38-
export const CodePreviewPanel = ({
38+
export const CodePreview = ({
3939
file,
4040
selectedMatchIndex,
4141
onSelectedMatchIndexChange,
4242
onClose,
43-
}: CodePreviewPanelProps) => {
43+
}: CodePreviewProps) => {
4444
const editorRef = useRef<ReactCodeMirrorRef>(null);
4545

4646
const [ keymapType ] = useKeymapType();
@@ -67,6 +67,7 @@ export const CodePreviewPanel = ({
6767
keymapExtension,
6868
gutterWidthExtension,
6969
syntaxHighlighting,
70+
EditorView.lineWrapping,
7071
searchResultHighlightExtension(),
7172
search({
7273
top: true,
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
'use client';
2+
3+
import { fetchFileSource } from "@/app/api/(client)/client";
4+
import { getCodeHostFilePreviewLink } from "@/lib/utils";
5+
import { useQuery } from "@tanstack/react-query";
6+
import { CodePreview, CodePreviewFile } from "./codePreview";
7+
import { SearchResultFile } from "@/lib/types";
8+
9+
interface CodePreviewPanelProps {
10+
fileMatch?: SearchResultFile;
11+
onClose: () => void;
12+
selectedMatchIndex: number;
13+
onSelectedMatchIndexChange: (index: number) => void;
14+
}
15+
16+
export const CodePreviewPanel = ({
17+
fileMatch,
18+
onClose,
19+
selectedMatchIndex,
20+
onSelectedMatchIndexChange,
21+
}: CodePreviewPanelProps) => {
22+
23+
const { data: file } = useQuery({
24+
queryKey: ["source", fileMatch?.FileName, fileMatch?.Repository],
25+
queryFn: async (): Promise<CodePreviewFile | undefined> => {
26+
if (!fileMatch) {
27+
return undefined;
28+
}
29+
30+
return fetchFileSource(fileMatch.FileName, fileMatch.Repository)
31+
.then(({ source }) => {
32+
// @todo : refector this to use the templates provided by zoekt.
33+
const link = getCodeHostFilePreviewLink(fileMatch.Repository, fileMatch.FileName)
34+
35+
const decodedSource = atob(source);
36+
37+
// Filter out filename matches
38+
const filteredMatches = fileMatch.ChunkMatches.filter((match) => {
39+
return !match.FileName;
40+
});
41+
42+
return {
43+
content: decodedSource,
44+
filepath: fileMatch.FileName,
45+
matches: filteredMatches,
46+
link: link,
47+
language: fileMatch.Language,
48+
};
49+
});
50+
},
51+
enabled: fileMatch !== undefined,
52+
});
53+
54+
return (
55+
<CodePreview
56+
file={file}
57+
onClose={onClose}
58+
selectedMatchIndex={selectedMatchIndex}
59+
onSelectedMatchIndexChange={onSelectedMatchIndexChange}
60+
/>
61+
)
62+
63+
}
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
'use client';
2+
3+
import { useExtensionWithDependency } from "@/hooks/useExtensionWithDependency";
4+
import { useSyntaxHighlightingExtension } from "@/hooks/useSyntaxHighlightingExtension";
5+
import { useThemeNormalized } from "@/hooks/useThemeNormalized";
6+
import { lineOffsetExtension } from "@/lib/extensions/lineOffsetExtension";
7+
import { SearchResultRange } from "@/lib/types";
8+
import CodeMirror, { Decoration, DecorationSet, EditorState, EditorView, ReactCodeMirrorRef, StateField, Transaction } from "@uiw/react-codemirror";
9+
import { useMemo, useRef } from "react";
10+
11+
const markDecoration = Decoration.mark({
12+
class: "cm-searchMatch-selected"
13+
});
14+
15+
interface CodePreviewProps {
16+
content: string,
17+
language: string,
18+
ranges: SearchResultRange[],
19+
lineOffset: number,
20+
}
21+
22+
export const CodePreview = ({
23+
content,
24+
language,
25+
ranges,
26+
lineOffset,
27+
}: CodePreviewProps) => {
28+
const editorRef = useRef<ReactCodeMirrorRef>(null);
29+
const { theme } = useThemeNormalized();
30+
31+
const syntaxHighlighting = useSyntaxHighlightingExtension(language, editorRef.current?.view);
32+
33+
const rangeHighlighting = useExtensionWithDependency(editorRef.current?.view ?? null, () => {
34+
return [
35+
StateField.define<DecorationSet>({
36+
create(editorState: EditorState) {
37+
const document = editorState.doc;
38+
39+
const decorations = ranges
40+
.sort((a, b) => {
41+
return a.Start.ByteOffset - b.Start.ByteOffset;
42+
})
43+
.filter(({ Start, End }) => {
44+
const startLine = Start.LineNumber - lineOffset;
45+
const endLine = End.LineNumber - lineOffset;
46+
47+
if (
48+
startLine < 1 ||
49+
endLine < 1 ||
50+
startLine > document.lines ||
51+
endLine > document.lines
52+
) {
53+
return false;
54+
}
55+
return true;
56+
})
57+
.map(({ Start, End }) => {
58+
const startLine = Start.LineNumber - lineOffset;
59+
const endLine = End.LineNumber - lineOffset;
60+
61+
const from = document.line(startLine).from + Start.Column - 1;
62+
const to = document.line(endLine).from + End.Column - 1;
63+
return markDecoration.range(from, to);
64+
});
65+
66+
return Decoration.set(decorations);
67+
},
68+
update(highlights: DecorationSet, _transaction: Transaction) {
69+
return highlights;
70+
},
71+
provide: (field) => EditorView.decorations.from(field),
72+
}),
73+
];
74+
}, [ranges, lineOffset]);
75+
76+
const extensions = useMemo(() => {
77+
return [
78+
syntaxHighlighting,
79+
EditorView.lineWrapping,
80+
lineOffsetExtension(lineOffset),
81+
rangeHighlighting,
82+
];
83+
}, [syntaxHighlighting, lineOffset, rangeHighlighting]);
84+
85+
return (
86+
<CodeMirror
87+
ref={editorRef}
88+
readOnly={true}
89+
editable={false}
90+
value={content}
91+
theme={theme === "dark" ? "dark" : "light"}
92+
basicSetup={{
93+
lineNumbers: true,
94+
syntaxHighlighting: true,
95+
96+
// Disable all this other stuff...
97+
... {
98+
foldGutter: false,
99+
highlightActiveLineGutter: false,
100+
highlightSpecialChars: false,
101+
history: false,
102+
drawSelection: false,
103+
dropCursor: false,
104+
allowMultipleSelections: false,
105+
indentOnInput: false,
106+
bracketMatching: false,
107+
closeBrackets: false,
108+
autocompletion: false,
109+
rectangularSelection: false,
110+
crosshairCursor: false,
111+
highlightActiveLine: false,
112+
highlightSelectionMatches: false,
113+
closeBracketsKeymap: false,
114+
defaultKeymap: false,
115+
searchKeymap: false,
116+
historyKeymap: false,
117+
foldKeymap: false,
118+
completionKeymap: false,
119+
lintKeymap: false,
120+
}
121+
}}
122+
extensions={extensions}
123+
/>
124+
)
125+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
'use client';
2+
3+
import { useMemo } from "react";
4+
import { CodePreview } from "./codePreview";
5+
import { SearchResultFile, SearchResultFileMatch } from "@/lib/types";
6+
7+
8+
interface FileMatchProps {
9+
match: SearchResultFileMatch;
10+
file: SearchResultFile;
11+
onOpen: () => void;
12+
}
13+
14+
export const FileMatch = ({
15+
match,
16+
file,
17+
onOpen,
18+
}: FileMatchProps) => {
19+
const content = useMemo(() => {
20+
return atob(match.Content);
21+
}, [match.Content]);
22+
23+
// If it's just the title, don't show a code preview
24+
if (match.FileName) {
25+
return null;
26+
}
27+
28+
return (
29+
<div
30+
tabIndex={0}
31+
className="cursor-pointer p-1 focus:ring-inset focus:ring-4 bg-white dark:bg-[#282c34]"
32+
onKeyDown={(e) => {
33+
if (e.key !== "Enter") {
34+
return;
35+
}
36+
onOpen();
37+
}}
38+
onClick={onOpen}
39+
>
40+
<CodePreview
41+
content={content}
42+
language={file.Language}
43+
ranges={match.Ranges}
44+
lineOffset={match.ContentStart.LineNumber - 1}
45+
/>
46+
</div>
47+
);
48+
}

0 commit comments

Comments
 (0)