Skip to content

Commit 7f952ce

Browse files
Search suggestions (#85)
The motivation for building search suggestions is two-fold: (1) to make the zoekt query language more approachable by presenting all available options to the user, and (2) make it easier for power-users to craft complex queries. The meat-n-potatoes of this change are concentrated in searchBar.tsx and searchSuggestionBox.tsx. The suggestions box works by maintaining a state-machine of "modes". By default, the box is in the refine mode, where suggestions for different prefixes (e.g., repo:, lang:, etc.) are suggested to the user. When one of these prefixes is matched, the state-machine transitions to the corresponding mode (e.g., repository, language, etc.) and surfaces suggestions for that mode (if any). The query is split up into parts by spaces " " (e.g., 'test repo:hello' -> ['test', 'repo:hello']). See splitQuery. The part that has the cursor over it is considered the active part. We evaluate which mode the state machine is in based on the active part. When a suggestion is clicked, we only modify the active part of the query. Three modes are currently missing suggestion data: file (file names), revision (branch / tag names), and symbol (symbol names). In future PRs, we will need to introduce endpoints into the backend to allow the frontend to fetch this data and surface it as suggestions..
1 parent 3fe2d32 commit 7f952ce

27 files changed

+2365
-211
lines changed

.github/workflows/test-web.yml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
name: Test Web
2+
3+
on:
4+
pull_request:
5+
branches: ["main"]
6+
7+
8+
jobs:
9+
build:
10+
runs-on: ubuntu-latest
11+
permissions:
12+
contents: read
13+
steps:
14+
- name: Checkout repository
15+
uses: actions/checkout@v4
16+
with:
17+
submodules: "true"
18+
- name: Use Node.Js
19+
uses: actions/setup-node@v4
20+
with:
21+
node-version: '20.x'
22+
23+
- name: Install
24+
run: yarn install --frozen-lockfile
25+
26+
- name: Test
27+
run: yarn workspace @sourcebot/web test
28+

.vscode/extensions.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
22
"recommendations": [
3-
"dbaeumer.vscode-eslint"
3+
"dbaeumer.vscode-eslint",
4+
"bradlc.vscode-tailwindcss"
45
]
56
}

.vscode/settings.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,16 @@
77
{
88
"pattern": "./packages/*/"
99
}
10+
],
11+
// @see : https://cva.style/docs/getting-started/installation#intellisense
12+
"tailwindCSS.experimental.classRegex": [
13+
[
14+
"cva\\(([^)]*)\\)",
15+
"[\"'`]([^\"'`]*).*?[\"'`]"
16+
],
17+
[
18+
"cx\\(([^)]*)\\)",
19+
"(?:'|\"|`)([^']*)(?:'|\"|`)"
20+
]
1021
]
1122
}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
],
66
"scripts": {
77
"build": "yarn workspaces run build",
8-
"test": "yarn workspace @sourcebot/backend test",
8+
"test": "yarn workspaces run test",
99
"dev": "npm-run-all --print-label --parallel dev:zoekt dev:backend dev:web",
1010
"dev:zoekt": "export PATH=\"$PWD/bin:$PATH\" && zoekt-webserver -index .sourcebot/index -rpc",
1111
"dev:backend": "yarn workspace @sourcebot/backend dev:watch",

packages/web/package.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
"dev": "next dev",
77
"build": "next build",
88
"start": "next start",
9-
"lint": "next lint"
9+
"lint": "next lint",
10+
"test": "vitest"
1011
},
1112
"dependencies": {
1213
"@codemirror/commands": "^6.6.0",
@@ -77,9 +78,12 @@
7778
"eslint-config-next": "14.2.6",
7879
"eslint-plugin-react": "^7.35.0",
7980
"eslint-plugin-react-hooks": "^4.6.2",
81+
"jsdom": "^25.0.1",
8082
"npm-run-all": "^4.1.5",
8183
"postcss": "^8",
8284
"tailwindcss": "^3.4.1",
83-
"typescript": "^5"
85+
"typescript": "^5",
86+
"vite-tsconfig-paths": "^5.1.3",
87+
"vitest": "^2.1.5"
8488
}
8589
}

packages/web/src/app/navigationMenu.tsx renamed to packages/web/src/app/components/navigationMenu.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ import { GitHubLogoIcon, DiscordLogoIcon } from "@radix-ui/react-icons";
77
import { SettingsDropdown } from "./settingsDropdown";
88
import { Separator } from "@/components/ui/separator";
99
import Image from "next/image";
10-
import logoDark from "../../public/sb_logo_dark_small.png";
11-
import logoLight from "../../public/sb_logo_light_small.png";
10+
import logoDark from "../../../public/sb_logo_dark_small.png";
11+
import logoLight from "../../../public/sb_logo_light_small.png";
1212
import { useRouter } from "next/navigation";
1313

1414
const SOURCEBOT_DISCORD_URL = "https://discord.gg/6Fhp27x7Pb";
Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
import { Suggestion, SuggestionMode } from "./searchSuggestionsBox";
2+
3+
/**
4+
* List of search prefixes that can be used while the
5+
* `refine` suggestion mode is active.
6+
*/
7+
enum SearchPrefix {
8+
repo = "repo:",
9+
r = "r:",
10+
lang = "lang:",
11+
file = "file:",
12+
rev = "rev:",
13+
revision = "revision:",
14+
b = "b:",
15+
branch = "branch:",
16+
sym = "sym:",
17+
content = "content:",
18+
archived = "archived:",
19+
case = "case:",
20+
fork = "fork:",
21+
public = "public:"
22+
}
23+
24+
const negate = (prefix: SearchPrefix) => {
25+
return `-${prefix}`;
26+
}
27+
28+
type SuggestionModeMapping = {
29+
suggestionMode: SuggestionMode,
30+
prefixes: string[],
31+
}
32+
33+
/**
34+
* Maps search prefixes to a suggestion mode. When a query starts
35+
* with a prefix, the corresponding suggestion mode is enabled.
36+
* @see [searchSuggestionsBox.tsx](./searchSuggestionsBox.tsx)
37+
*/
38+
export const suggestionModeMappings: SuggestionModeMapping[] = [
39+
{
40+
suggestionMode: "repo",
41+
prefixes: [
42+
SearchPrefix.repo, negate(SearchPrefix.repo),
43+
SearchPrefix.r, negate(SearchPrefix.r),
44+
]
45+
},
46+
{
47+
suggestionMode: "language",
48+
prefixes: [
49+
SearchPrefix.lang, negate(SearchPrefix.lang),
50+
]
51+
},
52+
{
53+
suggestionMode: "file",
54+
prefixes: [
55+
SearchPrefix.file, negate(SearchPrefix.file),
56+
]
57+
},
58+
{
59+
suggestionMode: "content",
60+
prefixes: [
61+
SearchPrefix.content, negate(SearchPrefix.content),
62+
]
63+
},
64+
{
65+
suggestionMode: "revision",
66+
prefixes: [
67+
SearchPrefix.rev, negate(SearchPrefix.rev),
68+
SearchPrefix.revision, negate(SearchPrefix.revision),
69+
SearchPrefix.branch, negate(SearchPrefix.branch),
70+
SearchPrefix.b, negate(SearchPrefix.b),
71+
]
72+
},
73+
{
74+
suggestionMode: "symbol",
75+
prefixes: [
76+
SearchPrefix.sym, negate(SearchPrefix.sym),
77+
]
78+
},
79+
{
80+
suggestionMode: "archived",
81+
prefixes: [
82+
SearchPrefix.archived
83+
]
84+
},
85+
{
86+
suggestionMode: "case",
87+
prefixes: [
88+
SearchPrefix.case
89+
]
90+
},
91+
{
92+
suggestionMode: "fork",
93+
prefixes: [
94+
SearchPrefix.fork
95+
]
96+
},
97+
{
98+
suggestionMode: "public",
99+
prefixes: [
100+
SearchPrefix.public
101+
]
102+
}
103+
];
104+
105+
export const refineModeSuggestions: Suggestion[] = [
106+
{
107+
value: SearchPrefix.repo,
108+
description: "Include only results from the given repository.",
109+
spotlight: true,
110+
},
111+
{
112+
value: negate(SearchPrefix.repo),
113+
description: "Exclude results from the given repository."
114+
},
115+
{
116+
value: SearchPrefix.lang,
117+
description: "Include only results from the given language.",
118+
spotlight: true,
119+
},
120+
{
121+
value: negate(SearchPrefix.lang),
122+
description: "Exclude results from the given language."
123+
},
124+
{
125+
value: SearchPrefix.file,
126+
description: "Include only results from filepaths matching the given search pattern.",
127+
spotlight: true,
128+
},
129+
{
130+
value: negate(SearchPrefix.file),
131+
description: "Exclude results from file paths matching the given search pattern."
132+
},
133+
{
134+
value: SearchPrefix.rev,
135+
description: "Search a given branch or tag instead of the default branch.",
136+
spotlight: true,
137+
},
138+
{
139+
value: negate(SearchPrefix.rev),
140+
description: "Exclude results from the given branch or tag."
141+
},
142+
{
143+
value: SearchPrefix.sym,
144+
description: "Include only symbols matching the given search pattern.",
145+
spotlight: true,
146+
},
147+
{
148+
value: negate(SearchPrefix.sym),
149+
description: "Exclude results from symbols matching the given search pattern."
150+
},
151+
{
152+
value: SearchPrefix.content,
153+
description: "Include only results from files if their content matches the given search pattern."
154+
},
155+
{
156+
value: negate(SearchPrefix.content),
157+
description: "Exclude results from files if their content matches the given search pattern."
158+
},
159+
{
160+
value: SearchPrefix.archived,
161+
description: "Include results from archived repositories.",
162+
},
163+
{
164+
value: SearchPrefix.case,
165+
description: "Control case-sensitivity of search patterns."
166+
},
167+
{
168+
value: SearchPrefix.fork,
169+
description: "Include only results from forked repositories."
170+
},
171+
{
172+
value: SearchPrefix.public,
173+
description: "Filter on repository visibility."
174+
},
175+
];
176+
177+
export const publicModeSuggestions: Suggestion[] = [
178+
{
179+
value: "yes",
180+
description: "Only include results from public repositories."
181+
},
182+
{
183+
value: "no",
184+
description: "Only include results from private repositories."
185+
},
186+
];
187+
188+
export const forkModeSuggestions: Suggestion[] = [
189+
{
190+
value: "yes",
191+
description: "Only include results from forked repositories."
192+
},
193+
{
194+
value: "no",
195+
description: "Only include results from non-forked repositories."
196+
},
197+
];
198+
199+
export const caseModeSuggestions: Suggestion[] = [
200+
{
201+
value: "auto",
202+
description: "Search patterns are case-insensitive if all characters are lowercase, and case sensitive otherwise (default)."
203+
},
204+
{
205+
value: "yes",
206+
description: "Case sensitive search."
207+
},
208+
{
209+
value: "no",
210+
description: "Case insensitive search."
211+
},
212+
];
213+
214+
export const archivedModeSuggestions: Suggestion[] = [
215+
{
216+
value: "yes",
217+
description: "Only include results in archived repositories."
218+
},
219+
{
220+
value: "no",
221+
description: "Only include results in non-archived repositories."
222+
},
223+
];
224+
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
2+
export { SearchBar } from "./searchBar";

0 commit comments

Comments
 (0)