Skip to content

Syntax reference guide #169

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Jan 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading