[Cherry Pick] Syntax reference guide (#169) (#216)

This commit is contained in:
Brendan Kellam 2025-02-27 17:02:58 -08:00 committed by GitHub
parent 1bdb65c34e
commit e1533f22f7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 316 additions and 74 deletions

View file

@ -31,7 +31,7 @@ export const FileHeader = ({
{info?.icon ? ( {info?.icon ? (
<Image <Image
src={info.icon} src={info.icon}
alt={info.costHostName} alt={info.codeHostName}
className={`w-4 h-4 ${info.iconClassName}`} className={`w-4 h-4 ${info.iconClassName}`}
/> />
): ( ): (

View file

@ -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>
)
}

View file

@ -63,7 +63,7 @@ const RepositoryBadge = ({
return { return {
repoIcon: <Image repoIcon: <Image
src={info.icon} src={info.icon}
alt={info.costHostName} alt={info.codeHostName}
className={`w-4 h-4 ${info.iconClassName}`} className={`w-4 h-4 ${info.iconClassName}`}
/>, />,
displayName: info.displayName, displayName: info.displayName,

View file

@ -43,6 +43,7 @@ import { Separator } from "@/components/ui/separator";
import { Tooltip, TooltipTrigger, TooltipContent } from "@/components/ui/tooltip"; import { Tooltip, TooltipTrigger, TooltipContent } from "@/components/ui/tooltip";
import { Toggle } from "@/components/ui/toggle"; import { Toggle } from "@/components/ui/toggle";
import { useDomain } from "@/hooks/useDomain"; import { useDomain } from "@/hooks/useDomain";
import { KeyboardShortcutHint } from "../keyboardShortcutHint";
interface SearchBarProps { interface SearchBarProps {
className?: string; className?: string;
@ -72,7 +73,7 @@ const searchBarKeymap: readonly KeyBinding[] = ([
] as KeyBinding[]).concat(historyKeymap); ] as KeyBinding[]).concat(historyKeymap);
const searchBarContainerVariants = cva( 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: { variants: {
size: { size: {
@ -266,6 +267,7 @@ export const SearchBar = ({
indentWithTab={false} indentWithTab={false}
autoFocus={autoFocus ?? false} autoFocus={autoFocus ?? false}
/> />
<KeyboardShortcutHint shortcut="/" />
<SearchSuggestionsBox <SearchSuggestionsBox
ref={suggestionBoxRef} ref={suggestionBoxRef}
query={query} query={query}

View file

@ -16,6 +16,7 @@ import { IconType } from "react-icons/lib";
import { VscFile, VscFilter, VscRepo, VscSymbolMisc } from "react-icons/vsc"; import { VscFile, VscFilter, VscRepo, VscSymbolMisc } from "react-icons/vsc";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
import { Separator } from "@/components/ui/separator"; import { Separator } from "@/components/ui/separator";
import { KeyboardShortcutHint } from "../keyboardShortcutHint";
export type Suggestion = { export type Suggestion = {
value: string; value: string;
@ -337,7 +338,7 @@ const SearchSuggestionsBox = forwardRef(({
onFocus={onFocus} onFocus={onFocus}
onBlur={onBlur} onBlur={onBlur}
> >
<p className="text-muted-foreground text-sm mb-1"> <p className="text-muted-foreground text-sm mb-2">
{suggestionModeText} {suggestionModeText}
</p> </p>
{isLoadingSuggestions ? ( {isLoadingSuggestions ? (
@ -385,19 +386,29 @@ const SearchSuggestionsBox = forwardRef(({
)} )}
</div> </div>
))} ))}
{isFocused && ( <Separator
<> orientation="horizontal"
<Separator className="my-2"
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">
<div className="flex flex-row items-center justify-end mt-1"> <p className="text-muted-foreground text-sm">
<span className="text-muted-foreground text-xs"> Syntax help:
Press <kbd className="font-mono text-xs font-bold">Enter</kbd> to select </p>
</span> <div className="flex flex-row gap-0.5 items-center">
<KeyboardShortcutHint shortcut="⌘" />
<KeyboardShortcutHint shortcut="/" />
</div> </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> </div>
) )
}); });

View file

@ -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>
)
}

View file

@ -10,6 +10,7 @@ import { cookies, headers } from "next/headers";
import { getSelectorsByUserAgent } from "react-device-detect"; import { getSelectorsByUserAgent } from "react-device-detect";
import { MobileUnsupportedSplashScreen } from "./components/mobileUnsupportedSplashScreen"; import { MobileUnsupportedSplashScreen } from "./components/mobileUnsupportedSplashScreen";
import { MOBILE_UNSUPPORTED_SPLASH_SCREEN_DISMISSED_COOKIE_NAME } from "@/lib/constants"; import { MOBILE_UNSUPPORTED_SPLASH_SCREEN_DISMISSED_COOKIE_NAME } from "@/lib/constants";
import { SyntaxReferenceGuide } from "./components/syntaxReferenceGuide";
interface LayoutProps { interface LayoutProps {
children: React.ReactNode, children: React.ReactNode,
@ -79,5 +80,10 @@ export default async function Layout({
<MobileUnsupportedSplashScreen /> <MobileUnsupportedSplashScreen />
) )
} }
return children; return (
<>
{children}
<SyntaxReferenceGuide />
</>
)
} }

View file

@ -8,6 +8,7 @@ import { PageNotFound } from "./components/pageNotFound";
import { Footer } from "./components/footer"; import { Footer } from "./components/footer";
import { SourcebotLogo } from "../components/sourcebotLogo"; import { SourcebotLogo } from "../components/sourcebotLogo";
import { RepositorySnapshot } from "./components/repositorySnapshot"; import { RepositorySnapshot } from "./components/repositorySnapshot";
import { KeyboardShortcutHint } from "./components/keyboardShortcutHint";
export default async function Home({ params: { domain } }: { params: { domain: string } }) { export default async function Home({ params: { domain } }: { params: { domain: string } }) {
const org = await getOrgFromDomain(domain); const org = await getOrgFromDomain(domain);
@ -87,6 +88,9 @@ export default async function Home({ params: { domain } }: { params: { domain: s
</QueryExample> </QueryExample>
</HowToSection> </HowToSection>
</div> </div>
<div className="text-sm">
<span className="dark:text-gray-300">Reference guide: </span><KeyboardShortcutHint shortcut="⌘" /> <KeyboardShortcutHint shortcut="/" />
</div>
</div> </div>
</div> </div>
<Footer /> <Footer />

View file

@ -33,7 +33,7 @@ export const FilterPanel = ({
const Icon = info ? ( const Icon = info ? (
<Image <Image
src={info.icon} src={info.icon}
alt={info.costHostName} alt={info.codeHostName}
className={cn('w-4 h-4 flex-shrink-0', info.iconClassName)} className={cn('w-4 h-4 flex-shrink-0', info.iconClassName)}
/> />
) : ( ) : (

View file

@ -36,7 +36,7 @@ export type CodeHostType = "github" | "gitlab" | "gitea" | "gerrit";
type CodeHostInfo = { type CodeHostInfo = {
type: CodeHostType; type: CodeHostType;
displayName: string; displayName: string;
costHostName: string; codeHostName: string;
repoLink: string; repoLink: string;
icon: string; icon: string;
iconClassName?: string; iconClassName?: string;
@ -54,52 +54,7 @@ export const getRepoCodeHostInfo = (repo?: Repository): CodeHostInfo | undefined
const url = new URL(repo.URL); const url = new URL(repo.URL);
const displayName = url.pathname.slice(1); const displayName = url.pathname.slice(1);
switch (webUrlType) { return _getCodeHostInfoInternal(webUrlType, displayName, repo.URL);
case 'github': {
const { src, className } = getCodeHostIcon('github')!;
return {
type: "github",
displayName: displayName,
costHostName: "GitHub",
repoLink: repo.URL,
icon: src,
iconClassName: className,
}
}
case 'gitlab': {
const { src, className } = getCodeHostIcon('gitlab')!;
return {
type: "gitlab",
displayName: displayName,
costHostName: "GitLab",
repoLink: repo.URL,
icon: src,
iconClassName: className,
}
}
case 'gitea': {
const { src, className } = getCodeHostIcon('gitea')!;
return {
type: "gitea",
displayName: displayName,
costHostName: "Gitea",
repoLink: repo.URL,
icon: src,
iconClassName: className,
}
}
case 'gitiles': {
const { src, className } = getCodeHostIcon('gerrit')!;
return {
type: "gerrit",
displayName: displayName,
costHostName: "Gerrit",
repoLink: repo.URL,
icon: src,
iconClassName: className,
}
}
}
} }
export const getRepoQueryCodeHostInfo = (repo?: RepositoryQuery): CodeHostInfo | undefined => { export const getRepoQueryCodeHostInfo = (repo?: RepositoryQuery): CodeHostInfo | undefined => {
@ -108,14 +63,18 @@ export const getRepoQueryCodeHostInfo = (repo?: RepositoryQuery): CodeHostInfo |
} }
const displayName = repo.repoName.split('/').slice(-2).join('/'); const displayName = repo.repoName.split('/').slice(-2).join('/');
switch (repo.codeHostType) { return _getCodeHostInfoInternal(repo.codeHostType, displayName, repo.repoCloneUrl);
}
const _getCodeHostInfoInternal = (type: string, displayName: string, cloneUrl: string): CodeHostInfo | undefined => {
switch (type) {
case 'github': { case 'github': {
const { src, className } = getCodeHostIcon('github')!; const { src, className } = getCodeHostIcon('github')!;
return { return {
type: "github", type: "github",
displayName: displayName, displayName: displayName,
costHostName: "GitHub", codeHostName: "GitHub",
repoLink: repo.repoCloneUrl, repoLink: cloneUrl,
icon: src, icon: src,
iconClassName: className, iconClassName: className,
} }
@ -125,8 +84,8 @@ export const getRepoQueryCodeHostInfo = (repo?: RepositoryQuery): CodeHostInfo |
return { return {
type: "gitlab", type: "gitlab",
displayName: displayName, displayName: displayName,
costHostName: "GitLab", codeHostName: "GitLab",
repoLink: repo.repoCloneUrl, repoLink: cloneUrl,
icon: src, icon: src,
iconClassName: className, iconClassName: className,
} }
@ -136,8 +95,8 @@ export const getRepoQueryCodeHostInfo = (repo?: RepositoryQuery): CodeHostInfo |
return { return {
type: "gitea", type: "gitea",
displayName: displayName, displayName: displayName,
costHostName: "Gitea", codeHostName: "Gitea",
repoLink: repo.repoCloneUrl, repoLink: cloneUrl,
icon: src, icon: src,
iconClassName: className, iconClassName: className,
} }
@ -147,8 +106,8 @@ export const getRepoQueryCodeHostInfo = (repo?: RepositoryQuery): CodeHostInfo |
return { return {
type: "gerrit", type: "gerrit",
displayName: displayName, displayName: displayName,
costHostName: "Gerrit", codeHostName: "Gerrit",
repoLink: repo.repoCloneUrl, repoLink: cloneUrl,
icon: src, icon: src,
iconClassName: className, iconClassName: className,
} }