Skip to content

Commit

Permalink
Syntax reference guide (#169)
Browse files Browse the repository at this point in the history
  • Loading branch information
brendan-kellam authored Jan 17, 2025
1 parent b96fffc commit 6c77278
Show file tree
Hide file tree
Showing 14 changed files with 544 additions and 41 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Added a syntax reference guide. The guide can be opened using the hotkey "Cmd + /" ("Ctrl + /" on Windows). ([#169](https://github.com/sourcebot-dev/sourcebot/pull/169))

## [2.7.1] - 2025-01-15

### Fixed
Expand Down
1 change: 1 addition & 0 deletions packages/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"@hookform/resolvers": "^3.9.0",
"@iconify/react": "^5.1.0",
"@iizukak/codemirror-lang-wgsl": "^0.3.0",
"@radix-ui/react-dialog": "^1.1.4",
"@radix-ui/react-dropdown-menu": "^2.1.1",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.1.0",
Expand Down
2 changes: 1 addition & 1 deletion packages/web/src/app/components/fireHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export const FileHeader = ({
{info?.icon ? (
<Image
src={info.icon}
alt={info.costHostName}
alt={info.codeHostName}
className={`w-4 h-4 ${info.iconClassName}`}
/>
): (
Expand Down
16 changes: 16 additions & 0 deletions packages/web/src/app/components/keyboardShortcutHint.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import React from 'react'

interface KeyboardShortcutHintProps {
shortcut: string
label?: string
}

export function KeyboardShortcutHint({ shortcut, label }: KeyboardShortcutHintProps) {
return (
<div className="inline-flex items-center" aria-label={label || `Keyboard shortcut: ${shortcut}`}>
<kbd className="px-2 py-1 text-xs font-semibold border rounded-md">
{shortcut}
</kbd>
</div>
)
}
2 changes: 1 addition & 1 deletion packages/web/src/app/components/repositoryCarousel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ const RepositoryBadge = ({
return {
repoIcon: <Image
src={info.icon}
alt={info.costHostName}
alt={info.codeHostName}
className={`w-4 h-4 ${info.iconClassName}`}
/>,
displayName: info.displayName,
Expand Down
4 changes: 3 additions & 1 deletion packages/web/src/app/components/searchBar/searchBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import { useSuggestionModeAndQuery } from "./useSuggestionModeAndQuery";
import { Separator } from "@/components/ui/separator";
import { Tooltip, TooltipTrigger, TooltipContent } from "@/components/ui/tooltip";
import { Toggle } from "@/components/ui/toggle";
import { KeyboardShortcutHint } from "../keyboardShortcutHint";

interface SearchBarProps {
className?: string;
Expand Down Expand Up @@ -71,7 +72,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: {
Expand Down Expand Up @@ -264,6 +265,7 @@ export const SearchBar = ({
indentWithTab={false}
autoFocus={autoFocus ?? false}
/>
<KeyboardShortcutHint shortcut="/" />
<SearchSuggestionsBox
ref={suggestionBoxRef}
query={query}
Expand Down
37 changes: 24 additions & 13 deletions packages/web/src/app/components/searchBar/searchSuggestionsBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { IconType } from "react-icons/lib";
import { VscFile, VscFilter, VscRepo, VscSymbolMisc } from "react-icons/vsc";
import { Skeleton } from "@/components/ui/skeleton";
import { Separator } from "@/components/ui/separator";
import { KeyboardShortcutHint } from "../keyboardShortcutHint";

export type Suggestion = {
value: string;
Expand Down Expand Up @@ -337,7 +338,7 @@ const SearchSuggestionsBox = forwardRef(({
onFocus={onFocus}
onBlur={onBlur}
>
<p className="text-muted-foreground text-sm mb-1">
<p className="text-muted-foreground text-sm mb-2">
{suggestionModeText}
</p>
{isLoadingSuggestions ? (
Expand Down Expand Up @@ -385,19 +386,29 @@ const SearchSuggestionsBox = forwardRef(({
)}
</div>
))}
{isFocused && (
<>
<Separator
orientation="horizontal"
className="my-2"
/>
<div className="flex flex-row items-center justify-end mt-1">
<span className="text-muted-foreground text-xs">
Press <kbd className="font-mono text-xs font-bold">Enter</kbd> to select
</span>
<Separator
orientation="horizontal"
className="my-2"
/>
<div className="flex flex-row items-center justify-between mt-1">
<div className="flex flex-row gap-1.5 items-center">
<p className="text-muted-foreground text-sm">
Syntax help:
</p>
<div className="flex flex-row gap-0.5 items-center">
<KeyboardShortcutHint shortcut="⌘" />
<KeyboardShortcutHint shortcut="/" />
</div>
</>
)}
</div>
{isFocused && (
<span className="flex flex-row gap-1.5 items-center">
<KeyboardShortcutHint shortcut="↵" />
<span className="text-muted-foreground text-sm font-medium">
to select
</span>
</span>
)}
</div>
</div>
)
});
Expand Down
244 changes: 244 additions & 0 deletions packages/web/src/app/components/syntaxReferenceGuide.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLElement | null>(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 (
<Dialog
open={isOpen}
onOpenChange={handleOpenChange}
>
<DialogContent
className="max-h-[80vh] max-w-[700px] overflow-scroll"
>
<DialogHeader>
<DialogTitle>Syntax Reference Guide</DialogTitle>
<DialogDescription className="text-sm text-foreground">
Queries consist of space-seperated regular expressions. Wrapping expressions in <Code>{`""`}</Code> combines them. By default, a file must have at least one match for each expression to be included.
</DialogDescription>
</DialogHeader>
<Table>
<TableHeader>
<TableRow>
<TableHead className="py-2">Example</TableHead>
<TableHead className="py-2">Explanation</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell className="py-2"><Code>foo</Code></TableCell>
<TableCell className="py-2">Match files with regex <Code>/foo/</Code></TableCell>
</TableRow>
<TableRow>
<TableCell className="py-2"><Code>foo bar</Code></TableCell>
<TableCell className="py-2">Match files with regex <Code>/foo/</Code> <b>and</b> <Code>/bar/</Code></TableCell>
</TableRow>
<TableRow>
<TableCell className="py-2"><Code>{`"foo bar"`}</Code></TableCell>
<TableCell className="py-2">Match files with regex <Code>/foo bar/</Code></TableCell>
</TableRow>
</TableBody>
</Table>

<Separator className="my-2"/>
<p className="text-sm">
{`Multiple expressions can be or'd together with `}<Code>or</Code>, negated with <Code>-</Code>, or grouped with <Code>()</Code>.
</p>
<Table>
<TableHeader>
<TableRow>
<TableHead className="py-2">Example</TableHead>
<TableHead className="py-2">Explanation</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell className="py-2"><Code>foo <Highlight>or</Highlight> bar</Code></TableCell>
<TableCell className="py-2">Match files with regex <Code>/foo/</Code> <b>or</b> <Code>/bar/</Code></TableCell>
</TableRow>
<TableRow>
<TableCell className="py-2"><Code>foo -bar</Code></TableCell>
<TableCell className="py-2">Match files with regex <Code>/foo/</Code> but <b>not</b> <Code>/bar/</Code></TableCell>
</TableRow>
<TableRow>
<TableCell className="py-2"><Code>foo (bar <Highlight>or</Highlight> baz)</Code></TableCell>
<TableCell className="py-2">Match files with regex <Code>/foo/</Code> <b>and</b> either <Code>/bar/</Code> <b>or</b> <Code>/baz/</Code></TableCell>
</TableRow>
</TableBody>
</Table>

<Separator className="my-2"/>
<p className="text-sm">
Expressions can be prefixed with certain keywords to modify search behavior. Some keywords can be negated using the <Code>-</Code> prefix.
</p>

<Table>
<TableHeader>
<TableRow>
<TableHead className="py-2">Prefix</TableHead>
<TableHead className="py-2">Description</TableHead>
<TableHead className="py-2 w-[175px]">Example</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell className="py-2"><Code><Highlight>file:</Highlight></Code></TableCell>
<TableCell className="py-2">Filter results from filepaths that match the regex. By default all files are searched.</TableCell>
<TableCell className="py-2">
<div className="flex flex-col gap-1">
<Code
title="Filter results to filepaths that match regex /README/"
>
<Highlight>file:</Highlight>README
</Code>
<Code
title="Filter results to filepaths that match regex /my file/"
>
<Highlight>file:</Highlight>{`"my file"`}
</Code>
<Code
title="Ignore results from filepaths match regex /test\.ts$/"
>
<Highlight>-file:</Highlight>test\.ts$
</Code>
</div>
</TableCell>
</TableRow>
<TableRow>
<TableCell className="py-2"><Code><Highlight>repo:</Highlight></Code></TableCell>
<TableCell className="py-2">Filter results from repos that match the regex. By default all repos are searched.</TableCell>
<TableCell className="py-2">
<div className="flex flex-col gap-1">
<Code
title="Filter results to repos that match regex /linux/"
>
<Highlight>repo:</Highlight>linux
</Code>
<Code
title="Ignore results from repos that match regex /^web\/.*/"
>
<Highlight>-repo:</Highlight>^web/.*
</Code>
</div>
</TableCell>
</TableRow>
<TableRow>
<TableCell className="py-2"><Code><Highlight>rev:</Highlight></Code></TableCell>
<TableCell className="py-2">Filter results from a specific branch or tag. By default <b>only</b> the default branch is searched.</TableCell>
<TableCell className="py-2">
<div className="flex flex-col gap-1">
<Code
title="Filter results to branches that match regex /beta/"
>
<Highlight>rev:</Highlight>beta
</Code>
</div>
</TableCell>
</TableRow>
<TableRow>
<TableCell className="py-2"><Code><Highlight>lang:</Highlight></Code></TableCell>
<TableCell className="py-2">Filter results by language (as defined by <Link className="text-blue-500" href={LINGUIST_LINK}>linguist</Link>). By default all languages are searched.</TableCell>
<TableCell className="py-2">
<div className="flex flex-col gap-1">
<Code
title="Filter results to TypeScript files"
>
<Highlight>lang:</Highlight>TypeScript
</Code>
<Code
title="Ignore results from YAML files"
>
<Highlight>-lang:</Highlight>YAML
</Code>
</div>
</TableCell>
</TableRow>
<TableRow>
<TableCell className="py-2"><Code><Highlight>sym:</Highlight></Code></TableCell>
<TableCell className="py-2">Match symbol definitions created by <Link className="text-blue-500" href={CTAGS_LINK}>universal ctags</Link> at index time.</TableCell>
<TableCell className="py-2">
<div className="flex flex-col gap-1">
<Code
title="Filter results to symbols that match regex /\bmain\b/"
>
<Highlight>sym:</Highlight>\bmain\b
</Code>
</div>
</TableCell>
</TableRow>
</TableBody>
</Table>
</DialogContent>
</Dialog>
)
}

const Code = ({ children, className, title }: { children: React.ReactNode, className?: string, title?: string }) => {
return (
<code
className={clsx("bg-gray-100 dark:bg-gray-700 w-fit rounded-md font-mono px-2 py-0.5", className)}
title={title}
>
{children}
</code>
)
}

const Highlight = ({ children }: { children: React.ReactNode }) => {
return (
<span className="text-highlight">
{children}
</span>
)
}
2 changes: 2 additions & 0 deletions packages/web/src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { QueryClientProvider } from "./queryClientProvider";
import { PHProvider } from "./posthogProvider";
import { Toaster } from "@/components/ui/toaster";
import { TooltipProvider } from "@/components/ui/tooltip";
import { SyntaxReferenceGuide } from "./components/syntaxReferenceGuide";

export const metadata: Metadata = {
title: "Sourcebot",
Expand Down Expand Up @@ -41,6 +42,7 @@ export default function RootLayout({
<Suspense>
{children}
</Suspense>
<SyntaxReferenceGuide />
</TooltipProvider>
</QueryClientProvider>
</ThemeProvider>
Expand Down
Loading

0 comments on commit 6c77278

Please sign in to comment.