mirror of
https://github.com/sourcebot-dev/sourcebot.git
synced 2025-12-12 12:25:22 +00:00
parent
1bdb65c34e
commit
e1533f22f7
10 changed files with 316 additions and 74 deletions
|
|
@ -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}`}
|
||||
/>
|
||||
): (
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
<KeyboardShortcutHint shortcut="/" />
|
||||
<SearchSuggestionsBox
|
||||
ref={suggestionBoxRef}
|
||||
query={query}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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 ? (
|
||||
|
|
@ -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>
|
||||
)
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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({
|
|||
<MobileUnsupportedSplashScreen />
|
||||
)
|
||||
}
|
||||
return children;
|
||||
return (
|
||||
<>
|
||||
{children}
|
||||
<SyntaxReferenceGuide />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
@ -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
|
|||
</QueryExample>
|
||||
</HowToSection>
|
||||
</div>
|
||||
<div className="text-sm">
|
||||
<span className="dark:text-gray-300">Reference guide: </span><KeyboardShortcutHint shortcut="⌘" /> <KeyboardShortcutHint shortcut="/" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Footer />
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ export const FilterPanel = ({
|
|||
const Icon = info ? (
|
||||
<Image
|
||||
src={info.icon}
|
||||
alt={info.costHostName}
|
||||
alt={info.codeHostName}
|
||||
className={cn('w-4 h-4 flex-shrink-0', info.iconClassName)}
|
||||
/>
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ export type CodeHostType = "github" | "gitlab" | "gitea" | "gerrit";
|
|||
type CodeHostInfo = {
|
||||
type: CodeHostType;
|
||||
displayName: string;
|
||||
costHostName: string;
|
||||
codeHostName: string;
|
||||
repoLink: string;
|
||||
icon: string;
|
||||
iconClassName?: string;
|
||||
|
|
@ -54,52 +54,7 @@ export const getRepoCodeHostInfo = (repo?: Repository): CodeHostInfo | undefined
|
|||
|
||||
const url = new URL(repo.URL);
|
||||
const displayName = url.pathname.slice(1);
|
||||
switch (webUrlType) {
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
return _getCodeHostInfoInternal(webUrlType, displayName, repo.URL);
|
||||
}
|
||||
|
||||
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('/');
|
||||
switch (repo.codeHostType) {
|
||||
return _getCodeHostInfoInternal(repo.codeHostType, displayName, repo.repoCloneUrl);
|
||||
}
|
||||
|
||||
const _getCodeHostInfoInternal = (type: string, displayName: string, cloneUrl: string): CodeHostInfo | undefined => {
|
||||
switch (type) {
|
||||
case 'github': {
|
||||
const { src, className } = getCodeHostIcon('github')!;
|
||||
return {
|
||||
type: "github",
|
||||
displayName: displayName,
|
||||
costHostName: "GitHub",
|
||||
repoLink: repo.repoCloneUrl,
|
||||
codeHostName: "GitHub",
|
||||
repoLink: cloneUrl,
|
||||
icon: src,
|
||||
iconClassName: className,
|
||||
}
|
||||
|
|
@ -125,8 +84,8 @@ export const getRepoQueryCodeHostInfo = (repo?: RepositoryQuery): CodeHostInfo |
|
|||
return {
|
||||
type: "gitlab",
|
||||
displayName: displayName,
|
||||
costHostName: "GitLab",
|
||||
repoLink: repo.repoCloneUrl,
|
||||
codeHostName: "GitLab",
|
||||
repoLink: cloneUrl,
|
||||
icon: src,
|
||||
iconClassName: className,
|
||||
}
|
||||
|
|
@ -136,8 +95,8 @@ export const getRepoQueryCodeHostInfo = (repo?: RepositoryQuery): CodeHostInfo |
|
|||
return {
|
||||
type: "gitea",
|
||||
displayName: displayName,
|
||||
costHostName: "Gitea",
|
||||
repoLink: repo.repoCloneUrl,
|
||||
codeHostName: "Gitea",
|
||||
repoLink: cloneUrl,
|
||||
icon: src,
|
||||
iconClassName: className,
|
||||
}
|
||||
|
|
@ -147,8 +106,8 @@ export const getRepoQueryCodeHostInfo = (repo?: RepositoryQuery): CodeHostInfo |
|
|||
return {
|
||||
type: "gerrit",
|
||||
displayName: displayName,
|
||||
costHostName: "Gerrit",
|
||||
repoLink: repo.repoCloneUrl,
|
||||
codeHostName: "Gerrit",
|
||||
repoLink: cloneUrl,
|
||||
icon: src,
|
||||
iconClassName: className,
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue