Skip to content

Commit 120d84a

Browse files
Symbol suggestions (#98)
1 parent b115218 commit 120d84a

File tree

9 files changed

+201
-75
lines changed

9 files changed

+201
-75
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
12+
- Added symbol suggestions as suggestion type. ([#98](https://github.com/sourcebot-dev/sourcebot/pull/98))
13+
1014
## [2.5.2] - 2024-11-27
1115

1216
### Fixed

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -392,3 +392,9 @@ Or if you are [building locally](#build-from-source), create a `.env.local` file
392392
SOURCEBOT_TELEMETRY_DISABLED=1
393393
NEXT_PUBLIC_SOURCEBOT_TELEMETRY_DISABLED=1
394394
```
395+
396+
## Attributions
397+
398+
Sourcebot makes use of the following libraries:
399+
400+
- [@vscode/codicons](https://github.com/microsoft/vscode-codicons) under the [CC BY 4.0 License](https://github.com/microsoft/vscode-codicons/blob/main/LICENSE).

packages/web/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
"react-dom": "^18",
6060
"react-hook-form": "^7.53.0",
6161
"react-hotkeys-hook": "^4.5.1",
62+
"react-icons": "^5.3.0",
6263
"react-resizable-panels": "^2.1.1",
6364
"server-only": "^0.0.1",
6465
"sharp": "^0.33.5",

packages/web/src/app/components/searchBar/searchBar.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -255,13 +255,16 @@ export const SearchBar = ({
255255
setIsSuggestionsBoxFocused(document.activeElement === suggestionBoxRef.current);
256256
}}
257257
cursorPosition={cursorPosition}
258-
data={suggestionData}
259-
onSuggestionModeChanged={(suggestionMode) => {
260-
setSuggestionMode(suggestionMode);
258+
onSuggestionModeChanged={(newSuggestionMode) => {
259+
if (suggestionMode !== newSuggestionMode) {
260+
console.debug(`Suggestion mode changed: ${suggestionMode} -> ${newSuggestionMode}`);
261+
}
262+
setSuggestionMode(newSuggestionMode);
261263
}}
262264
onSuggestionQueryChanged={(suggestionQuery) => {
263265
setSuggestionQuery(suggestionQuery);
264266
}}
267+
{...suggestionData}
265268
/>
266269
</div>
267270
)

packages/web/src/app/components/searchBar/searchSuggestionsBox.tsx

Lines changed: 84 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
'use client';
22

33
import { isDefined } from "@/lib/utils";
4-
import { CommitIcon, MixerVerticalIcon } from "@radix-ui/react-icons";
5-
import { IconProps } from "@radix-ui/react-icons/dist/types";
64
import assert from "assert";
75
import clsx from "clsx";
86
import escapeStringRegexp from "escape-string-regexp";
@@ -16,13 +14,14 @@ import {
1614
refineModeSuggestions,
1715
suggestionModeMappings
1816
} from "./constants";
19-
20-
type Icon = React.ForwardRefExoticComponent<IconProps & React.RefAttributes<SVGSVGElement>>;
17+
import { IconType } from "react-icons/lib";
18+
import { VscFile, VscFilter, VscRepo, VscSymbolMisc } from "react-icons/vsc";
2119

2220
export type Suggestion = {
2321
value: string;
2422
description?: string;
2523
spotlight?: boolean;
24+
Icon?: IconType;
2625
}
2726

2827
export type SuggestionMode =
@@ -50,30 +49,42 @@ interface SearchSuggestionsBoxProps {
5049
onSuggestionModeChanged: (suggestionMode: SuggestionMode) => void;
5150
onSuggestionQueryChanged: (suggestionQuery: string) => void;
5251

53-
data: {
54-
repos: Suggestion[];
55-
languages: Suggestion[];
56-
files: Suggestion[];
57-
}
52+
isLoadingSuggestions: boolean;
53+
repoSuggestions: Suggestion[];
54+
fileSuggestions: Suggestion[];
55+
symbolSuggestions: Suggestion[];
56+
languageSuggestions: Suggestion[];
5857
}
5958

6059
const SearchSuggestionsBox = forwardRef(({
6160
query,
6261
onCompletion,
6362
isEnabled,
64-
data,
6563
cursorPosition,
6664
isFocused,
6765
onFocus,
6866
onBlur,
6967
onReturnFocus,
7068
onSuggestionModeChanged,
7169
onSuggestionQueryChanged,
70+
isLoadingSuggestions,
71+
repoSuggestions,
72+
fileSuggestions,
73+
symbolSuggestions,
74+
languageSuggestions,
7275
}: SearchSuggestionsBoxProps, ref: Ref<HTMLDivElement>) => {
7376

7477
const [highlightedSuggestionIndex, setHighlightedSuggestionIndex] = useState(0);
7578

7679
const { suggestionQuery, suggestionMode } = useMemo<{ suggestionQuery?: string, suggestionMode?: SuggestionMode }>(() => {
80+
// Only re-calculate the suggestion mode and query if the box is enabled.
81+
// This is to avoid transitioning the suggestion mode and causing a fetch
82+
// when it is not needed.
83+
// @see: useSuggestionsData.ts
84+
if (!isEnabled) {
85+
return {};
86+
}
87+
7788
const { queryParts, cursorIndex } = splitQuery(query, cursorPosition);
7889
if (queryParts.length === 0) {
7990
return {};
@@ -107,10 +118,10 @@ const SearchSuggestionsBox = forwardRef(({
107118
suggestionQuery: part,
108119
suggestionMode: "refine",
109120
}
110-
}, [cursorPosition, query]);
121+
}, [cursorPosition, isEnabled, query]);
111122

112-
const { suggestions, isHighlightEnabled, Icon, onSuggestionClicked } = useMemo(() => {
113-
if (!isDefined(suggestionQuery) || !isDefined(suggestionMode)) {
123+
const { suggestions, isHighlightEnabled, DefaultIcon, onSuggestionClicked } = useMemo(() => {
124+
if (!isEnabled || !isDefined(suggestionQuery) || !isDefined(suggestionMode)) {
114125
return {};
115126
}
116127

@@ -144,7 +155,7 @@ const SearchSuggestionsBox = forwardRef(({
144155
isSpotlightEnabled = false,
145156
isClientSideSearchEnabled = true,
146157
onSuggestionClicked,
147-
Icon,
158+
DefaultIcon,
148159
} = ((): {
149160
threshold?: number,
150161
limit?: number,
@@ -153,7 +164,7 @@ const SearchSuggestionsBox = forwardRef(({
153164
isSpotlightEnabled?: boolean,
154165
isClientSideSearchEnabled?: boolean,
155166
onSuggestionClicked: (value: string) => void,
156-
Icon?: Icon
167+
DefaultIcon?: IconType
157168
} => {
158169
switch (suggestionMode) {
159170
case "public":
@@ -178,13 +189,13 @@ const SearchSuggestionsBox = forwardRef(({
178189
}
179190
case "repo":
180191
return {
181-
list: data.repos,
182-
Icon: CommitIcon,
192+
list: repoSuggestions,
193+
DefaultIcon: VscRepo,
183194
onSuggestionClicked: createOnSuggestionClickedHandler({ regexEscaped: true }),
184195
}
185196
case "language": {
186197
return {
187-
list: data.languages,
198+
list: languageSuggestions,
188199
onSuggestionClicked: createOnSuggestionClickedHandler(),
189200
isSpotlightEnabled: true,
190201
}
@@ -195,18 +206,25 @@ const SearchSuggestionsBox = forwardRef(({
195206
list: refineModeSuggestions,
196207
isHighlightEnabled: true,
197208
isSpotlightEnabled: true,
198-
Icon: MixerVerticalIcon,
209+
DefaultIcon: VscFilter,
199210
onSuggestionClicked: createOnSuggestionClickedHandler({ trailingSpace: false }),
200211
}
201212
case "file":
202213
return {
203-
list: data.files,
214+
list: fileSuggestions,
215+
onSuggestionClicked: createOnSuggestionClickedHandler(),
216+
isClientSideSearchEnabled: false,
217+
DefaultIcon: VscFile,
218+
}
219+
case "symbol":
220+
return {
221+
list: symbolSuggestions,
204222
onSuggestionClicked: createOnSuggestionClickedHandler(),
205223
isClientSideSearchEnabled: false,
224+
DefaultIcon: VscSymbolMisc,
206225
}
207226
case "revision":
208227
case "content":
209-
case "symbol":
210228
return {
211229
list: [],
212230
onSuggestionClicked: createOnSuggestionClickedHandler(),
@@ -252,11 +270,11 @@ const SearchSuggestionsBox = forwardRef(({
252270
return {
253271
suggestions,
254272
isHighlightEnabled,
255-
Icon,
273+
DefaultIcon,
256274
onSuggestionClicked,
257275
}
258276

259-
}, [suggestionQuery, suggestionMode, query, cursorPosition, onCompletion, data.repos, data.files, data.languages]);
277+
}, [isEnabled, suggestionQuery, suggestionMode, query, cursorPosition, onCompletion, repoSuggestions, fileSuggestions, symbolSuggestions, languageSuggestions]);
260278

261279
// When the list of suggestions change, reset the highlight index
262280
useEffect(() => {
@@ -283,20 +301,29 @@ const SearchSuggestionsBox = forwardRef(({
283301
case "repo":
284302
return "Repositories";
285303
case "refine":
286-
return "Refine search"
304+
return "Refine search";
305+
case "file":
306+
return "Files";
307+
case "symbol":
308+
return "Symbols";
309+
case "language":
310+
return "Languages";
287311
default:
288312
return "";
289313
}
290314
}, [suggestionMode]);
291315

292316
if (
293317
!isEnabled ||
294-
!suggestions ||
295-
suggestions.length === 0
318+
!suggestions
296319
) {
297320
return null;
298321
}
299322

323+
if (suggestions.length === 0 && !isLoadingSuggestions) {
324+
return null;
325+
}
326+
300327
return (
301328
<div
302329
ref={ref}
@@ -305,6 +332,9 @@ const SearchSuggestionsBox = forwardRef(({
305332
onKeyDown={(e) => {
306333
if (e.key === 'Enter') {
307334
e.stopPropagation();
335+
if (highlightedSuggestionIndex < 0 || highlightedSuggestionIndex >= suggestions.length) {
336+
return;
337+
}
308338
const value = suggestions[highlightedSuggestionIndex].value;
309339
onSuggestionClicked(value);
310340
}
@@ -334,7 +364,17 @@ const SearchSuggestionsBox = forwardRef(({
334364
<p className="text-muted-foreground text-sm mb-1">
335365
{suggestionModeText}
336366
</p>
337-
{suggestions.map((result, index) => (
367+
{isLoadingSuggestions ? (
368+
// Skeleton placeholder
369+
<div className="animate-pulse flex flex-col gap-2 px-1 py-0.5">
370+
{
371+
Array.from({ length: 10 }).map((_, index) => (
372+
<div key={index} className="h-4 bg-muted rounded-md w-full"></div>
373+
))
374+
}
375+
</div>
376+
) : suggestions.map((result, index) => (
377+
// Suggestion list
338378
<div
339379
key={index}
340380
className={clsx("flex flex-row items-center font-mono text-sm hover:bg-muted rounded-md px-1 py-0.5 cursor-pointer", {
@@ -345,23 +385,24 @@ const SearchSuggestionsBox = forwardRef(({
345385
onSuggestionClicked(result.value)
346386
}}
347387
>
348-
{Icon && (
349-
<Icon className="w-3 h-3 mr-2" />
350-
)}
351-
<div className="flex flex-row items-center">
352-
<span
353-
className={clsx('mr-2 flex-none', {
354-
"text-highlight": isHighlightEnabled
355-
})}
356-
>
357-
{result.value}
388+
{result.Icon ? (
389+
<result.Icon className="w-3 h-3 mr-2 flex-none" />
390+
) : DefaultIcon ? (
391+
<DefaultIcon className="w-3 h-3 mr-2 flex-none" />
392+
) : null}
393+
<span
394+
className={clsx('mr-2', {
395+
"text-highlight": isHighlightEnabled,
396+
"truncate": !result.description,
397+
})}
398+
>
399+
{result.value}
400+
</span>
401+
{result.description && (
402+
<span className="text-muted-foreground font-light">
403+
{result.description}
358404
</span>
359-
{result.description && (
360-
<span className="text-muted-foreground font-light">
361-
{result.description}
362-
</span>
363-
)}
364-
</div>
405+
)}
365406
</div>
366407
))}
367408
{isFocused && (

0 commit comments

Comments
 (0)