Basic syntax highlighting support for search bar (#66)

This commit is contained in:
Brendan Kellam 2024-11-12 18:43:44 -08:00 committed by GitHub
parent 9cba4f274f
commit 276086d2d6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 165 additions and 57 deletions

View file

@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Changed
- Added support for syntax highlighting in the search bar. ([#66](https://github.com/sourcebot-dev/sourcebot/pull/66))
## [2.4.1] - 2024-11-11
### Added

View file

@ -39,6 +39,7 @@
"@tanstack/react-query": "^5.53.3",
"@tanstack/react-table": "^8.20.5",
"@tanstack/react-virtual": "^3.10.8",
"@uiw/codemirror-themes": "^4.23.6",
"@uiw/react-codemirror": "^4.23.0",
"class-variance-authority": "^0.7.0",
"client-only": "^0.0.1",

View file

@ -29,6 +29,7 @@
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
--highlight: 224, 76%, 48%;
}
.dark {
@ -56,6 +57,7 @@
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
--highlight: 217, 91%, 60%;
}
}
@ -87,6 +89,10 @@
border: solid;
}
.cm-editor.cm-focused {
outline: none !important;
}
.truncate-start {
direction: rtl;
text-align: left;

View file

@ -148,7 +148,7 @@ const HowToSection = ({ title, children }: { title: string, children: React.Reac
const Highlight = ({ children }: { children: React.ReactNode }) => {
return (
<span className="text-blue-700 dark:text-blue-500">
<span className="text-highlight">
{children}
</span>
)

View file

@ -1,22 +1,39 @@
'use client';
import {
Form,
FormControl,
FormField,
FormItem,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { useTailwind } from "@/hooks/useTailwind";
import { SearchQueryParams } from "@/lib/types";
import { cn, createPathWithQueryParams } from "@/lib/utils";
import { zodResolver } from "@hookform/resolvers/zod";
import {
cursorCharLeft,
cursorCharRight,
cursorDocEnd,
cursorDocStart,
cursorLineBoundaryBackward,
cursorLineBoundaryForward,
deleteCharBackward,
deleteCharForward,
deleteGroupBackward,
deleteGroupForward,
deleteLineBoundaryBackward,
deleteLineBoundaryForward,
history,
historyKeymap,
selectAll,
selectCharLeft,
selectCharRight,
selectDocEnd,
selectDocStart,
selectLineBoundaryBackward,
selectLineBoundaryForward
} from "@codemirror/commands";
import { LanguageSupport, StreamLanguage } from "@codemirror/language";
import { tags as t } from '@lezer/highlight';
import { createTheme } from '@uiw/codemirror-themes';
import CodeMirror, { KeyBinding, keymap, ReactCodeMirrorRef } from "@uiw/react-codemirror";
import { cva } from "class-variance-authority";
import { useRouter } from "next/navigation";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { useHotkeys } from 'react-hotkeys-hook'
import { useRef } from "react";
import { SearchQueryParams } from "@/lib/types";
import { useMemo, useRef, useState } from "react";
import { useHotkeys } from 'react-hotkeys-hook';
interface SearchBarProps {
className?: string;
@ -25,12 +42,53 @@ interface SearchBarProps {
autoFocus?: boolean;
}
const formSchema = z.object({
query: z.string(),
const searchBarKeymap: readonly KeyBinding[] = ([
{ key: "ArrowLeft", run: cursorCharLeft, shift: selectCharLeft, preventDefault: true },
{ key: "ArrowRight", run: cursorCharRight, shift: selectCharRight, preventDefault: true },
{ key: "Home", run: cursorLineBoundaryBackward, shift: selectLineBoundaryBackward, preventDefault: true },
{ key: "Mod-Home", run: cursorDocStart, shift: selectDocStart },
{ key: "End", run: cursorLineBoundaryForward, shift: selectLineBoundaryForward, preventDefault: true },
{ key: "Mod-End", run: cursorDocEnd, shift: selectDocEnd },
{ key: "Mod-a", run: selectAll },
{ key: "Backspace", run: deleteCharBackward, shift: deleteCharBackward },
{ key: "Delete", run: deleteCharForward },
{ key: "Mod-Backspace", mac: "Alt-Backspace", run: deleteGroupBackward },
{ key: "Mod-Delete", mac: "Alt-Delete", run: deleteGroupForward },
{ mac: "Mod-Backspace", run: deleteLineBoundaryBackward },
{ mac: "Mod-Delete", run: deleteLineBoundaryForward }
] as KeyBinding[]).concat(historyKeymap);
const zoektLanguage = StreamLanguage.define({
token: (stream) => {
if (stream.match(/-?(file|branch|revision|rev|case|repo|lang|content|sym):/)) {
return t.keyword.toString();
}
if (stream.match(/\bor\b/)) {
return t.keyword.toString();
}
stream.next();
return null;
},
});
const zoekt = () =>{
return new LanguageSupport(zoektLanguage);
}
const extensions = [
keymap.of(searchBarKeymap),
history(),
zoekt()
];
const searchBarVariants = cva(
"w-full",
"flex items-center w-full p-0.5 border rounded-md",
{
variants: {
size: {
@ -42,7 +100,7 @@ const searchBarVariants = cva(
size: "default",
}
}
)
);
export const SearchBar = ({
className,
@ -50,55 +108,71 @@ export const SearchBar = ({
defaultQuery,
autoFocus,
}: SearchBarProps) => {
const router = useRouter();
const tailwind = useTailwind();
const theme = useMemo(() => {
return createTheme({
theme: 'light',
settings: {
background: tailwind.theme.colors.background,
foreground: tailwind.theme.colors.foreground,
caret: '#AEAFAD',
},
styles: [
{
tag: t.keyword,
color: tailwind.theme.colors.highlight,
},
],
});
}, [tailwind]);
const [query, setQuery] = useState(defaultQuery ?? "");
const editorRef = useRef<ReactCodeMirrorRef>(null);
const inputRef = useRef<HTMLInputElement>(null);
useHotkeys('/', (event) => {
event.preventDefault();
inputRef.current?.focus();
editorRef.current?.view?.focus();
if (editorRef.current?.view) {
cursorDocEnd({
state: editorRef.current.view.state,
dispatch: editorRef.current.view.dispatch,
});
const router = useRouter();
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
query: defaultQuery ?? "",
}
});
const onSubmit = (values: z.infer<typeof formSchema>) => {
const onSubmit = () => {
const url = createPathWithQueryParams('/search',
[SearchQueryParams.query, values.query],
[SearchQueryParams.query, query],
)
router.push(url);
}
return (
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="w-full"
>
<FormField
control={form.control}
name="query"
render={( { field }) => (
<FormItem>
<FormControl>
<Input
placeholder="Search..."
<div
className={cn(searchBarVariants({ size, className }))}
{...field}
ref={inputRef}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
onSubmit();
}
}}
>
<CodeMirror
ref={editorRef}
className="grow"
placeholder={"Search..."}
value={query}
onChange={(value) => {
setQuery(value);
}}
theme={theme}
basicSetup={false}
extensions={extensions}
indentWithTab={false}
autoFocus={autoFocus ?? false}
// This is needed to prevent mobile browsers from zooming in when the input is focused
style={{ fontSize: '1rem' }}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
</div>
)
}

View file

@ -0,0 +1,13 @@
'use client';
import { useMemo } from "react";
import resolveConfig from 'tailwindcss/resolveConfig';
import tailwindConfig from '../../tailwind.config';
export const useTailwind = () => {
const tailwind = useMemo(() => {
return resolveConfig(tailwindConfig);
}, [tailwindConfig]);
return tailwind;
}

View file

@ -52,6 +52,7 @@ const config = {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
highlight: "hsl(var(--highlight))",
},
borderRadius: {
lg: "var(--radius)",

View file

@ -1743,6 +1743,15 @@
"@codemirror/state" "^6.0.0"
"@codemirror/view" "^6.0.0"
"@uiw/codemirror-themes@^4.23.6":
version "4.23.6"
resolved "https://registry.yarnpkg.com/@uiw/codemirror-themes/-/codemirror-themes-4.23.6.tgz#47a101733a9c4aa382696178bc4b7bc0ecf0e5fa"
integrity sha512-0dpuLQW+V6zrKvfvor/eo71V3tpr2L2Hsu8QZAdtSzksjWABxTOzH3ShaBRxCEsrz6sU9sa9o7ShwBMMDz59bQ==
dependencies:
"@codemirror/language" "^6.0.0"
"@codemirror/state" "^6.0.0"
"@codemirror/view" "^6.0.0"
"@uiw/react-codemirror@^4.23.0":
version "4.23.5"
resolved "https://registry.yarnpkg.com/@uiw/react-codemirror/-/react-codemirror-4.23.5.tgz#6a16c23062067732cba105ac33ad69cf8e5c2111"