diff --git a/packages/web/src/app/[domain]/components/fireHeader.tsx b/packages/web/src/app/[domain]/components/fireHeader.tsx index 702cce58..b5df9e14 100644 --- a/packages/web/src/app/[domain]/components/fireHeader.tsx +++ b/packages/web/src/app/[domain]/components/fireHeader.tsx @@ -31,7 +31,7 @@ export const FileHeader = ({ {info?.icon ? ( {info.costHostName} ): ( diff --git a/packages/web/src/app/[domain]/components/keyboardShortcutHint.tsx b/packages/web/src/app/[domain]/components/keyboardShortcutHint.tsx new file mode 100644 index 00000000..f93209f1 --- /dev/null +++ b/packages/web/src/app/[domain]/components/keyboardShortcutHint.tsx @@ -0,0 +1,16 @@ +import React from 'react' + +interface KeyboardShortcutHintProps { + shortcut: string + label?: string +} + +export function KeyboardShortcutHint({ shortcut, label }: KeyboardShortcutHintProps) { + return ( +
+ + {shortcut} + +
+ ) +} diff --git a/packages/web/src/app/[domain]/components/repositoryCarousel.tsx b/packages/web/src/app/[domain]/components/repositoryCarousel.tsx index 038b9bbf..7d1c3a65 100644 --- a/packages/web/src/app/[domain]/components/repositoryCarousel.tsx +++ b/packages/web/src/app/[domain]/components/repositoryCarousel.tsx @@ -63,7 +63,7 @@ const RepositoryBadge = ({ return { repoIcon: {info.costHostName}, displayName: info.displayName, diff --git a/packages/web/src/app/[domain]/components/searchBar/searchBar.tsx b/packages/web/src/app/[domain]/components/searchBar/searchBar.tsx index d038d905..6981c843 100644 --- a/packages/web/src/app/[domain]/components/searchBar/searchBar.tsx +++ b/packages/web/src/app/[domain]/components/searchBar/searchBar.tsx @@ -43,6 +43,7 @@ import { Separator } from "@/components/ui/separator"; import { Tooltip, TooltipTrigger, TooltipContent } from "@/components/ui/tooltip"; import { Toggle } from "@/components/ui/toggle"; import { useDomain } from "@/hooks/useDomain"; +import { KeyboardShortcutHint } from "../keyboardShortcutHint"; interface SearchBarProps { className?: string; @@ -72,7 +73,7 @@ const searchBarKeymap: readonly KeyBinding[] = ([ ] as KeyBinding[]).concat(historyKeymap); const searchBarContainerVariants = cva( - "search-bar-container flex items-center py-0.5 px-1 border rounded-md relative", + "search-bar-container flex items-center justify-center py-0.5 px-2 border rounded-md relative", { variants: { size: { @@ -266,6 +267,7 @@ export const SearchBar = ({ indentWithTab={false} autoFocus={autoFocus ?? false} /> + -

+

{suggestionModeText}

{isLoadingSuggestions ? ( @@ -385,19 +386,29 @@ const SearchSuggestionsBox = forwardRef(({ )} ))} - {isFocused && ( - <> - -
- - Press Enter to select - + +
+
+

+ Syntax help: +

+
+ +
- - )} +
+ {isFocused && ( + + + + to select + + + )} +
) }); diff --git a/packages/web/src/app/[domain]/components/syntaxReferenceGuide.tsx b/packages/web/src/app/[domain]/components/syntaxReferenceGuide.tsx new file mode 100644 index 00000000..d2225799 --- /dev/null +++ b/packages/web/src/app/[domain]/components/syntaxReferenceGuide.tsx @@ -0,0 +1,244 @@ +'use client'; + +import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"; +import { Separator } from "@/components/ui/separator"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import clsx from "clsx"; +import Link from "next/link"; +import { useCallback, useRef, useState } from "react"; +import { useHotkeys } from "react-hotkeys-hook"; + +const LINGUIST_LINK = "https://github.com/github-linguist/linguist/blob/main/lib/linguist/languages.yml"; +const CTAGS_LINK = "https://ctags.io/"; + +export const SyntaxReferenceGuide = () => { + const [isOpen, setIsOpen] = useState(false); + const previousFocusedElement = useRef(null); + + const openDialog = useCallback(() => { + previousFocusedElement.current = document.activeElement as HTMLElement; + setIsOpen(true); + }, []); + + const closeDialog = useCallback(() => { + setIsOpen(false); + + // @note: Without requestAnimationFrame, focus was not being returned + // to codemirror elements for some reason. + requestAnimationFrame(() => { + previousFocusedElement.current?.focus(); + }); + }, []); + + const handleOpenChange = useCallback((isOpen: boolean) => { + if (isOpen) { + openDialog(); + } else { + closeDialog(); + } + }, [closeDialog, openDialog]); + + useHotkeys("mod+/", (event) => { + event.preventDefault(); + handleOpenChange(!isOpen); + }, { + enableOnFormTags: true, + enableOnContentEditable: true, + description: "Open Syntax Reference Guide", + }); + + return ( + + + + Syntax Reference Guide + + Queries consist of space-seperated regular expressions. Wrapping expressions in {`""`} combines them. By default, a file must have at least one match for each expression to be included. + + + + + + Example + Explanation + + + + + foo + Match files with regex /foo/ + + + foo bar + Match files with regex /foo/ and /bar/ + + + {`"foo bar"`} + Match files with regex /foo bar/ + + +
+ + +

+ {`Multiple expressions can be or'd together with `}or, negated with -, or grouped with (). +

+ + + + Example + Explanation + + + + + foo or bar + Match files with regex /foo/ or /bar/ + + + foo -bar + Match files with regex /foo/ but not /bar/ + + + foo (bar or baz) + Match files with regex /foo/ and either /bar/ or /baz/ + + +
+ + +

+ Expressions can be prefixed with certain keywords to modify search behavior. Some keywords can be negated using the - prefix. +

+ + + + + Prefix + Description + Example + + + + + file: + Filter results from filepaths that match the regex. By default all files are searched. + +
+ + file:README + + + file:{`"my file"`} + + + -file:test\.ts$ + +
+
+
+ + repo: + Filter results from repos that match the regex. By default all repos are searched. + +
+ + repo:linux + + + -repo:^web/.* + +
+
+
+ + rev: + Filter results from a specific branch or tag. By default only the default branch is searched. + +
+ + rev:beta + +
+
+
+ + lang: + Filter results by language (as defined by linguist). By default all languages are searched. + +
+ + lang:TypeScript + + + -lang:YAML + +
+
+
+ + sym: + Match symbol definitions created by universal ctags at index time. + +
+ + sym:\bmain\b + +
+
+
+
+
+
+
+ ) +} + +const Code = ({ children, className, title }: { children: React.ReactNode, className?: string, title?: string }) => { + return ( + + {children} + + ) +} + +const Highlight = ({ children }: { children: React.ReactNode }) => { + return ( + + {children} + + ) +} diff --git a/packages/web/src/app/[domain]/layout.tsx b/packages/web/src/app/[domain]/layout.tsx index cc739d35..7aefbbc9 100644 --- a/packages/web/src/app/[domain]/layout.tsx +++ b/packages/web/src/app/[domain]/layout.tsx @@ -10,6 +10,7 @@ import { cookies, headers } from "next/headers"; import { getSelectorsByUserAgent } from "react-device-detect"; import { MobileUnsupportedSplashScreen } from "./components/mobileUnsupportedSplashScreen"; import { MOBILE_UNSUPPORTED_SPLASH_SCREEN_DISMISSED_COOKIE_NAME } from "@/lib/constants"; +import { SyntaxReferenceGuide } from "./components/syntaxReferenceGuide"; interface LayoutProps { children: React.ReactNode, @@ -79,5 +80,10 @@ export default async function Layout({ ) } - return children; + return ( + <> + {children} + + + ) } \ No newline at end of file diff --git a/packages/web/src/app/[domain]/page.tsx b/packages/web/src/app/[domain]/page.tsx index 2f9c8aae..5b374b1c 100644 --- a/packages/web/src/app/[domain]/page.tsx +++ b/packages/web/src/app/[domain]/page.tsx @@ -8,6 +8,7 @@ import { PageNotFound } from "./components/pageNotFound"; import { Footer } from "./components/footer"; import { SourcebotLogo } from "../components/sourcebotLogo"; import { RepositorySnapshot } from "./components/repositorySnapshot"; +import { KeyboardShortcutHint } from "./components/keyboardShortcutHint"; export default async function Home({ params: { domain } }: { params: { domain: string } }) { const org = await getOrgFromDomain(domain); @@ -87,6 +88,9 @@ export default async function Home({ params: { domain } }: { params: { domain: s +
+ Reference guide: +