From 276086d2d67e17d8fecef45ae70847b677d0b49f Mon Sep 17 00:00:00 2001 From: Brendan Kellam Date: Tue, 12 Nov 2024 18:43:44 -0800 Subject: [PATCH] Basic syntax highlighting support for search bar (#66) --- CHANGELOG.md | 4 + packages/web/package.json | 1 + packages/web/src/app/globals.css | 6 + packages/web/src/app/page.tsx | 2 +- packages/web/src/app/searchBar.tsx | 186 ++++++++++++++++++-------- packages/web/src/hooks/useTailwind.ts | 13 ++ packages/web/tailwind.config.ts | 1 + yarn.lock | 9 ++ 8 files changed, 165 insertions(+), 57 deletions(-) create mode 100644 packages/web/src/hooks/useTailwind.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e68add2..a20d7ccf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/packages/web/package.json b/packages/web/package.json index 38443ca5..4fc540bf 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -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", diff --git a/packages/web/src/app/globals.css b/packages/web/src/app/globals.css index d1ff0cbb..13502f6d 100644 --- a/packages/web/src/app/globals.css +++ b/packages/web/src/app/globals.css @@ -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; diff --git a/packages/web/src/app/page.tsx b/packages/web/src/app/page.tsx index 0881f132..542d80c7 100644 --- a/packages/web/src/app/page.tsx +++ b/packages/web/src/app/page.tsx @@ -148,7 +148,7 @@ const HowToSection = ({ title, children }: { title: string, children: React.Reac const Highlight = ({ children }: { children: React.ReactNode }) => { return ( - + {children} ) diff --git a/packages/web/src/app/searchBar.tsx b/packages/web/src/app/searchBar.tsx index ffb2a3d1..23ffb6ac 100644 --- a/packages/web/src/app/searchBar.tsx +++ b/packages/web/src/app/searchBar.tsx @@ -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(null); - const inputRef = useRef(null); useHotkeys('/', (event) => { event.preventDefault(); - inputRef.current?.focus(); - }); - - const router = useRouter(); - const form = useForm>({ - resolver: zodResolver(formSchema), - defaultValues: { - query: defaultQuery ?? "", + editorRef.current?.view?.focus(); + if (editorRef.current?.view) { + cursorDocEnd({ + state: editorRef.current.view.state, + dispatch: editorRef.current.view.dispatch, + }); } }); - const onSubmit = (values: z.infer) => { + const onSubmit = () => { const url = createPathWithQueryParams('/search', - [SearchQueryParams.query, values.query], + [SearchQueryParams.query, query], ) router.push(url); } return ( -
- - ( - - - - - - - )} - /> - - +
{ + if (e.key === 'Enter') { + e.preventDefault(); + onSubmit(); + } + }} + > + { + setQuery(value); + }} + theme={theme} + basicSetup={false} + extensions={extensions} + indentWithTab={false} + autoFocus={autoFocus ?? false} + /> +
) } \ No newline at end of file diff --git a/packages/web/src/hooks/useTailwind.ts b/packages/web/src/hooks/useTailwind.ts new file mode 100644 index 00000000..9c72ebec --- /dev/null +++ b/packages/web/src/hooks/useTailwind.ts @@ -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; +} \ No newline at end of file diff --git a/packages/web/tailwind.config.ts b/packages/web/tailwind.config.ts index 8b7dd592..9ef8846a 100644 --- a/packages/web/tailwind.config.ts +++ b/packages/web/tailwind.config.ts @@ -52,6 +52,7 @@ const config = { DEFAULT: "hsl(var(--card))", foreground: "hsl(var(--card-foreground))", }, + highlight: "hsl(var(--highlight))", }, borderRadius: { lg: "var(--radius)", diff --git a/yarn.lock b/yarn.lock index b8b78507..6a7c0a90 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"