Skip to content

Commit e913b22

Browse files
Improve rendering performance of search results (#52)
1 parent 8a619b7 commit e913b22

File tree

14 files changed

+362
-151
lines changed

14 files changed

+362
-151
lines changed

.vscode/extensions.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"recommendations": [
3+
"dbaeumer.vscode-eslint"
4+
]
5+
}

.vscode/settings.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,10 @@
22
"files.associations": {
33
"*.json": "jsonc",
44
"index.json": "json"
5-
}
5+
},
6+
"eslint.workingDirectories": [
7+
{
8+
"pattern": "./packages/*/"
9+
}
10+
]
611
}

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1414
### Fixed
1515

1616
- Fixed issue with GitLab sub-projects not being included recursively. ([#54](https://github.com/sourcebot-dev/sourcebot/pull/54))
17+
- Fixed slow rendering performance when rendering a large number of results. ([#52](https://github.com/sourcebot-dev/sourcebot/pull/52))
1718

1819
## [2.1.1] - 2024-10-25
1920

packages/web/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
"@replit/codemirror-vim": "^6.2.1",
3939
"@tanstack/react-query": "^5.53.3",
4040
"@tanstack/react-table": "^8.20.5",
41+
"@tanstack/react-virtual": "^3.10.8",
4142
"@uiw/react-codemirror": "^4.23.0",
4243
"class-variance-authority": "^0.7.0",
4344
"client-only": "^0.0.1",

packages/web/src/app/search/components/codePreviewPanel/index.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,5 +59,4 @@ export const CodePreviewPanel = ({
5959
onSelectedMatchIndexChange={onSelectedMatchIndexChange}
6060
/>
6161
)
62-
6362
}

packages/web/src/app/search/components/filterPanel/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ export const FilterPanel = ({
9393
);
9494

9595
onFilterChanged(filteredMatches);
96-
}, [matches, repos, languages]);
96+
}, [matches, repos, languages, onFilterChanged]);
9797

9898
return (
9999
<div className="p-3 flex flex-col gap-3">
Lines changed: 26 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
'use client';
22

3-
import { useExtensionWithDependency } from "@/hooks/useExtensionWithDependency";
4-
import { useSyntaxHighlightingExtension } from "@/hooks/useSyntaxHighlightingExtension";
5-
import { useThemeNormalized } from "@/hooks/useThemeNormalized";
3+
import { getSyntaxHighlightingExtension } from "@/hooks/useSyntaxHighlightingExtension";
64
import { lineOffsetExtension } from "@/lib/extensions/lineOffsetExtension";
75
import { SearchResultRange } from "@/lib/types";
8-
import CodeMirror, { Decoration, DecorationSet, EditorState, EditorView, ReactCodeMirrorRef, StateField, Transaction } from "@uiw/react-codemirror";
6+
import { defaultHighlightStyle, syntaxHighlighting } from "@codemirror/language";
7+
import { EditorState, StateField, Transaction } from "@codemirror/state";
8+
import { defaultLightThemeOption, oneDarkHighlightStyle, oneDarkTheme } from "@uiw/react-codemirror";
9+
import { Decoration, DecorationSet, EditorView, lineNumbers } from "@codemirror/view";
910
import { useMemo, useRef } from "react";
11+
import { LightweightCodeMirror, CodeMirrorRef } from "./lightweightCodeMirror";
12+
import { useThemeNormalized } from "@/hooks/useThemeNormalized";
1013

1114
const markDecoration = Decoration.mark({
1215
class: "cm-searchMatch-selected"
@@ -25,13 +28,22 @@ export const CodePreview = ({
2528
ranges,
2629
lineOffset,
2730
}: CodePreviewProps) => {
28-
const editorRef = useRef<ReactCodeMirrorRef>(null);
31+
const editorRef = useRef<CodeMirrorRef>(null);
2932
const { theme } = useThemeNormalized();
3033

31-
const syntaxHighlighting = useSyntaxHighlightingExtension(language, editorRef.current?.view);
32-
33-
const rangeHighlighting = useExtensionWithDependency(editorRef.current?.view ?? null, () => {
34+
const extensions = useMemo(() => {
3435
return [
36+
EditorView.editable.of(false),
37+
...(theme === 'dark' ? [
38+
syntaxHighlighting(oneDarkHighlightStyle),
39+
oneDarkTheme,
40+
] : [
41+
syntaxHighlighting(defaultHighlightStyle),
42+
defaultLightThemeOption,
43+
]),
44+
lineNumbers(),
45+
lineOffsetExtension(lineOffset),
46+
getSyntaxHighlightingExtension(language),
3547
StateField.define<DecorationSet>({
3648
create(editorState: EditorState) {
3749
const document = editorState.doc;
@@ -61,7 +73,8 @@ export const CodePreview = ({
6173
const from = document.line(startLine).from + Start.Column - 1;
6274
const to = document.line(endLine).from + End.Column - 1;
6375
return markDecoration.range(from, to);
64-
});
76+
})
77+
.sort((a, b) => a.from - b.from);
6578

6679
return Decoration.set(decorations);
6780
},
@@ -70,56 +83,15 @@ export const CodePreview = ({
7083
},
7184
provide: (field) => EditorView.decorations.from(field),
7285
}),
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]);
86+
]
87+
}, [language, lineOffset, ranges, theme]);
8488

8589
return (
86-
<CodeMirror
90+
<LightweightCodeMirror
8791
ref={editorRef}
88-
readOnly={true}
89-
editable={false}
9092
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-
}}
12293
extensions={extensions}
12394
/>
12495
)
96+
12597
}

packages/web/src/app/search/components/searchResultsPanel/fileMatch.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export const FileMatch = ({
2929
return (
3030
<div
3131
tabIndex={0}
32-
className="cursor-pointer p-1 focus:ring-inset focus:ring-4 bg-white dark:bg-[#282c34]"
32+
className="cursor-pointer focus:ring-inset focus:ring-4 bg-white dark:bg-[#282c34]"
3333
onKeyDown={(e) => {
3434
if (e.key !== "Enter") {
3535
return;

packages/web/src/app/search/components/searchResultsPanel/fileMatchContainer.tsx

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,32 @@
11
'use client';
22

33
import { getRepoCodeHostInfo } from "@/lib/utils";
4-
import { useCallback, useMemo, useState } from "react";
4+
import { useCallback, useMemo } from "react";
55
import Image from "next/image";
66
import { DoubleArrowDownIcon, DoubleArrowUpIcon, FileIcon } from "@radix-ui/react-icons";
77
import clsx from "clsx";
88
import { Separator } from "@/components/ui/separator";
99
import { SearchResultFile } from "@/lib/types";
1010
import { FileMatch } from "./fileMatch";
1111

12-
const MAX_MATCHES_TO_PREVIEW = 3;
12+
export const MAX_MATCHES_TO_PREVIEW = 3;
1313

1414
interface FileMatchContainerProps {
1515
file: SearchResultFile;
1616
onOpenFile: () => void;
1717
onMatchIndexChanged: (matchIndex: number) => void;
18+
showAllMatches: boolean;
19+
onShowAllMatchesButtonClicked: () => void;
1820
}
1921

2022
export const FileMatchContainer = ({
2123
file,
2224
onOpenFile,
2325
onMatchIndexChanged,
26+
showAllMatches,
27+
onShowAllMatchesButtonClicked,
2428
}: FileMatchContainerProps) => {
2529

26-
const [showAll, setShowAll] = useState(false);
2730
const matchCount = useMemo(() => {
2831
return file.ChunkMatches.length;
2932
}, [file]);
@@ -33,12 +36,12 @@ export const FileMatchContainer = ({
3336
return a.ContentStart.LineNumber - b.ContentStart.LineNumber;
3437
});
3538

36-
if (!showAll) {
39+
if (!showAllMatches) {
3740
return sortedMatches.slice(0, MAX_MATCHES_TO_PREVIEW);
3841
}
3942

4043
return sortedMatches;
41-
}, [file, showAll]);
44+
}, [file, showAllMatches]);
4245

4346
const fileNameRange = useMemo(() => {
4447
for (const match of matches) {
@@ -79,10 +82,6 @@ export const FileMatchContainer = ({
7982
return matchCount > MAX_MATCHES_TO_PREVIEW;
8083
}, [matchCount]);
8184

82-
const onShowMoreMatches = useCallback(() => {
83-
setShowAll(!showAll);
84-
}, [showAll]);
85-
8685
const onOpenMatch = useCallback((index: number) => {
8786
const matchIndex = matches.slice(0, index).reduce((acc, match) => {
8887
return acc + match.Ranges.length;
@@ -94,8 +93,9 @@ export const FileMatchContainer = ({
9493

9594
return (
9695
<div>
96+
{/* Title */}
9797
<div
98-
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 cursor-pointer z-10"
98+
className="top-0 bg-cyan-200 dark:bg-cyan-900 primary-foreground px-2 py-0.5 flex flex-row items-center justify-between cursor-pointer"
9999
onClick={() => {
100100
onOpenFile();
101101
}}
@@ -132,6 +132,8 @@ export const FileMatchContainer = ({
132132
</div>
133133
</div>
134134
</div>
135+
136+
{/* Matches */}
135137
{matches.map((match, index) => (
136138
<div
137139
key={index}
@@ -148,6 +150,8 @@ export const FileMatchContainer = ({
148150
)}
149151
</div>
150152
))}
153+
154+
{/* Show more button */}
151155
{isMoreContentButtonVisible && (
152156
<div
153157
tabIndex={0}
@@ -156,15 +160,15 @@ export const FileMatchContainer = ({
156160
if (e.key !== "Enter") {
157161
return;
158162
}
159-
onShowMoreMatches();
163+
onShowAllMatchesButtonClicked();
160164
}}
161-
onClick={onShowMoreMatches}
165+
onClick={onShowAllMatchesButtonClicked}
162166
>
163167
<p
164168
className="text-blue-500 cursor-pointer text-sm flex flex-row items-center gap-2"
165169
>
166-
{showAll ? <DoubleArrowUpIcon className="w-3 h-3" /> : <DoubleArrowDownIcon className="w-3 h-3" />}
167-
{showAll ? `Show fewer matches` : `Show ${matchCount - MAX_MATCHES_TO_PREVIEW} more matches`}
170+
{showAllMatches ? <DoubleArrowUpIcon className="w-3 h-3" /> : <DoubleArrowDownIcon className="w-3 h-3" />}
171+
{showAllMatches ? `Show fewer matches` : `Show ${matchCount - MAX_MATCHES_TO_PREVIEW} more matches`}
168172
</p>
169173
</div>
170174
)}

0 commit comments

Comments
 (0)