This commit is contained in:
bkellam 2025-11-30 13:53:30 -08:00
parent 41a6eb48a0
commit 29994d6011
8 changed files with 100 additions and 32 deletions

View file

@ -10,6 +10,7 @@ import { useBrowseParams } from "./hooks/useBrowseParams";
import { FileSearchCommandDialog } from "./components/fileSearchCommandDialog"; import { FileSearchCommandDialog } from "./components/fileSearchCommandDialog";
import { useDomain } from "@/hooks/useDomain"; import { useDomain } from "@/hooks/useDomain";
import { SearchBar } from "../components/searchBar"; import { SearchBar } from "../components/searchBar";
import escapeStringRegexp from "escape-string-regexp";
interface LayoutProps { interface LayoutProps {
children: React.ReactNode; children: React.ReactNode;
@ -30,7 +31,7 @@ export default function Layout({
<SearchBar <SearchBar
size="sm" size="sm"
defaults={{ defaults={{
query: `repo:${repoName}${revisionName ? ` rev:${revisionName}` : ''} `, query: `repo:^${escapeStringRegexp(repoName)}$${revisionName ? ` rev:${revisionName}` : ''} `,
}} }}
className="w-full" className="w-full"
/> />

View file

@ -1,13 +1,15 @@
import { cn } from '@/lib/utils'
import React from 'react' import React from 'react'
interface KeyboardShortcutHintProps { interface KeyboardShortcutHintProps {
shortcut: string shortcut: string
label?: string label?: string
className?: string
} }
export function KeyboardShortcutHint({ shortcut, label }: KeyboardShortcutHintProps) { export function KeyboardShortcutHint({ shortcut, label, className }: KeyboardShortcutHintProps) {
return ( return (
<div className="inline-flex items-center" aria-label={label || `Keyboard shortcut: ${shortcut}`}> <div className={cn("inline-flex items-center", className)} aria-label={label || `Keyboard shortcut: ${shortcut}`}>
<kbd <kbd
className="px-2 py-1 font-semibold font-sans border rounded-md" className="px-2 py-1 font-semibold font-sans border rounded-md"
style={{ style={{

View file

@ -7,7 +7,7 @@ import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
const toggleVariants = cva( const toggleVariants = cva(
"inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 gap-2", "inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 gap-2 cursor-pointer",
{ {
variants: { variants: {
variant: { variant: {
@ -16,7 +16,7 @@ const toggleVariants = cva(
"border border-input bg-transparent hover:bg-accent hover:text-accent-foreground", "border border-input bg-transparent hover:bg-accent hover:text-accent-foreground",
}, },
size: { size: {
default: "h-10 px-3 min-w-10", default: "h-7 w-7 min-w-7 p-0",
sm: "h-9 px-2.5 min-w-9", sm: "h-9 px-2.5 min-w-9",
lg: "h-11 px-5 min-w-11", lg: "h-11 px-5 min-w-11",
}, },

View file

@ -1,19 +1,22 @@
'use client'; 'use client';
import { useBrowseState } from "@/app/[domain]/browse/hooks/useBrowseState"; import { useBrowseState } from "@/app/[domain]/browse/hooks/useBrowseState";
import { findSearchBasedSymbolReferences, findSearchBasedSymbolDefinitions} from "@/app/api/(client)/client"; import { findSearchBasedSymbolDefinitions, findSearchBasedSymbolReferences } from "@/app/api/(client)/client";
import { AnimatedResizableHandle } from "@/components/ui/animatedResizableHandle"; import { AnimatedResizableHandle } from "@/components/ui/animatedResizableHandle";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable"; import { ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable";
import { Toggle } from "@/components/ui/toggle";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { useDomain } from "@/hooks/useDomain"; import { useDomain } from "@/hooks/useDomain";
import { unwrapServiceError } from "@/lib/utils"; import { unwrapServiceError } from "@/lib/utils";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import clsx from "clsx"; import clsx from "clsx";
import { Loader2 } from "lucide-react"; import { GlobeIcon, Loader2 } from "lucide-react";
import { useMemo } from "react"; import { useMemo, useState } from "react";
import { VscSymbolMisc } from "react-icons/vsc"; import { VscSymbolMisc } from "react-icons/vsc";
import { ReferenceList } from "./referenceList"; import { ReferenceList } from "./referenceList";
import { KeyboardShortcutHint } from "@/app/components/keyboardShortcutHint";
import { useHotkeys } from "react-hotkeys-hook";
interface ExploreMenuProps { interface ExploreMenuProps {
selectedSymbolInfo: { selectedSymbolInfo: {
@ -34,18 +37,21 @@ export const ExploreMenu = ({
updateBrowseState, updateBrowseState,
} = useBrowseState(); } = useBrowseState();
const [isGlobalSearchEnabled, setIsGlobalSearchEnabled] = useState(false);
const { const {
data: referencesResponse, data: referencesResponse,
isError: isReferencesResponseError, isError: isReferencesResponseError,
isPending: isReferencesResponsePending, isPending: isReferencesResponsePending,
isLoading: isReferencesResponseLoading, isLoading: isReferencesResponseLoading,
} = useQuery({ } = useQuery({
queryKey: ["references", selectedSymbolInfo.symbolName, selectedSymbolInfo.repoName, selectedSymbolInfo.revisionName, selectedSymbolInfo.language, domain], queryKey: ["references", selectedSymbolInfo.symbolName, selectedSymbolInfo.repoName, selectedSymbolInfo.revisionName, selectedSymbolInfo.language, domain, isGlobalSearchEnabled],
queryFn: () => unwrapServiceError( queryFn: () => unwrapServiceError(
findSearchBasedSymbolReferences({ findSearchBasedSymbolReferences({
symbolName: selectedSymbolInfo.symbolName, symbolName: selectedSymbolInfo.symbolName,
language: selectedSymbolInfo.language, language: selectedSymbolInfo.language,
revisionName: selectedSymbolInfo.revisionName, revisionName: selectedSymbolInfo.revisionName,
repoName: isGlobalSearchEnabled ? undefined : selectedSymbolInfo.repoName
}) })
), ),
}); });
@ -56,16 +62,25 @@ export const ExploreMenu = ({
isPending: isDefinitionsResponsePending, isPending: isDefinitionsResponsePending,
isLoading: isDefinitionsResponseLoading, isLoading: isDefinitionsResponseLoading,
} = useQuery({ } = useQuery({
queryKey: ["definitions", selectedSymbolInfo.symbolName, selectedSymbolInfo.repoName, selectedSymbolInfo.revisionName, selectedSymbolInfo.language, domain], queryKey: ["definitions", selectedSymbolInfo.symbolName, selectedSymbolInfo.repoName, selectedSymbolInfo.revisionName, selectedSymbolInfo.language, domain, isGlobalSearchEnabled],
queryFn: () => unwrapServiceError( queryFn: () => unwrapServiceError(
findSearchBasedSymbolDefinitions({ findSearchBasedSymbolDefinitions({
symbolName: selectedSymbolInfo.symbolName, symbolName: selectedSymbolInfo.symbolName,
language: selectedSymbolInfo.language, language: selectedSymbolInfo.language,
revisionName: selectedSymbolInfo.revisionName, revisionName: selectedSymbolInfo.revisionName,
repoName: isGlobalSearchEnabled ? undefined : selectedSymbolInfo.repoName
}) })
), ),
}); });
useHotkeys('shift+a', () => {
setIsGlobalSearchEnabled(!isGlobalSearchEnabled);
}, {
enableOnFormTags: true,
enableOnContentEditable: true,
description: "Search all repositories",
});
const isPending = isReferencesResponsePending || isDefinitionsResponsePending; const isPending = isReferencesResponsePending || isDefinitionsResponsePending;
const isLoading = isReferencesResponseLoading || isDefinitionsResponseLoading; const isLoading = isReferencesResponseLoading || isDefinitionsResponseLoading;
const isError = isDefinitionsResponseError || isReferencesResponseError; const isError = isDefinitionsResponseError || isReferencesResponseError;
@ -98,29 +113,52 @@ export const ExploreMenu = ({
<ResizablePanel <ResizablePanel
minSize={10} minSize={10}
maxSize={20} maxSize={20}
className="flex flex-col h-full"
> >
<div className="flex flex-col p-2"> <div className="flex flex-col p-2">
<Tooltip <div className="flex flex-row items-center justify-between">
delayDuration={100}
> <Tooltip
<TooltipTrigger delayDuration={100}
disabled={true}
className="mr-auto"
> >
<Badge <TooltipTrigger
variant="outline" disabled={true}
className="w-fit h-fit flex-shrink-0 select-none" className="mr-auto"
> >
Search Based <Badge
</Badge> variant="outline"
</TooltipTrigger> className="w-fit h-fit flex-shrink-0 select-none"
<TooltipContent >
side="top" Search Based
align="start" </Badge>
> </TooltipTrigger>
Symbol references and definitions found using a best-guess search heuristic. <TooltipContent
</TooltipContent> side="top"
</Tooltip> align="center"
>
Symbol references and definitions found using a best-guess search heuristic.
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<span>
<Toggle
pressed={isGlobalSearchEnabled}
onPressedChange={setIsGlobalSearchEnabled}
>
<GlobeIcon className="w-4 h-4" />
</Toggle>
</span>
</TooltipTrigger>
<TooltipContent side="top" align="center">
{isGlobalSearchEnabled ? "Search in current repository only" : "Search all repositories"}
<KeyboardShortcutHint
shortcut="⇧ A"
className="ml-2"
/>
</TooltipContent>
</Tooltip>
</div>
<div className="flex flex-col gap-1 mt-4"> <div className="flex flex-col gap-1 mt-4">
<Entry <Entry
name="References" name="References"

View file

@ -26,8 +26,9 @@ export const findSymbolReferencesTool = tool({
inputSchema: z.object({ inputSchema: z.object({
symbol: z.string().describe("The symbol to find references to"), symbol: z.string().describe("The symbol to find references to"),
language: z.string().describe("The programming language of the symbol"), language: z.string().describe("The programming language of the symbol"),
repository: z.string().describe("The repository to scope the search to").optional(),
}), }),
execute: async ({ symbol, language }) => { execute: async ({ symbol, language, repository }) => {
// @todo: make revision configurable. // @todo: make revision configurable.
const revision = "HEAD"; const revision = "HEAD";
@ -35,6 +36,7 @@ export const findSymbolReferencesTool = tool({
symbolName: symbol, symbolName: symbol,
language, language,
revisionName: "HEAD", revisionName: "HEAD",
repoName: repository,
}); });
if (isServiceError(response)) { if (isServiceError(response)) {
@ -63,8 +65,9 @@ export const findSymbolDefinitionsTool = tool({
inputSchema: z.object({ inputSchema: z.object({
symbol: z.string().describe("The symbol to find definitions of"), symbol: z.string().describe("The symbol to find definitions of"),
language: z.string().describe("The programming language of the symbol"), language: z.string().describe("The programming language of the symbol"),
repository: z.string().describe("The repository to scope the search to").optional(),
}), }),
execute: async ({ symbol, language }) => { execute: async ({ symbol, language, repository }) => {
// @todo: make revision configurable. // @todo: make revision configurable.
const revision = "HEAD"; const revision = "HEAD";
@ -72,6 +75,7 @@ export const findSymbolDefinitionsTool = tool({
symbolName: symbol, symbolName: symbol,
language, language,
revisionName: revision, revisionName: revision,
repoName: repository,
}); });
if (isServiceError(response)) { if (isServiceError(response)) {

View file

@ -8,6 +8,7 @@ import { withOptionalAuthV2 } from "@/withAuthV2";
import { SearchResponse } from "../search/types"; import { SearchResponse } from "../search/types";
import { FindRelatedSymbolsRequest, FindRelatedSymbolsResponse } from "./types"; import { FindRelatedSymbolsRequest, FindRelatedSymbolsResponse } from "./types";
import { QueryIR } from '../search/ir'; import { QueryIR } from '../search/ir';
import escapeStringRegexp from "escape-string-regexp";
// The maximum number of matches to return from the search API. // The maximum number of matches to return from the search API.
const MAX_REFERENCE_COUNT = 1000; const MAX_REFERENCE_COUNT = 1000;
@ -18,6 +19,7 @@ export const findSearchBasedSymbolReferences = async (props: FindRelatedSymbolsR
symbolName, symbolName,
language, language,
revisionName = "HEAD", revisionName = "HEAD",
repoName,
} = props; } = props;
const languageFilter = getExpandedLanguageFilter(language); const languageFilter = getExpandedLanguageFilter(language);
@ -40,6 +42,11 @@ export const findSearchBasedSymbolReferences = async (props: FindRelatedSymbolsR
} }
}, },
languageFilter, languageFilter,
...(repoName ? [{
repo: {
regexp: `^${escapeStringRegexp(repoName)}$`,
}
}]: [])
] ]
} }
} }
@ -67,6 +74,7 @@ export const findSearchBasedSymbolDefinitions = async (props: FindRelatedSymbols
symbolName, symbolName,
language, language,
revisionName = "HEAD", revisionName = "HEAD",
repoName
} = props; } = props;
const languageFilter = getExpandedLanguageFilter(language); const languageFilter = getExpandedLanguageFilter(language);
@ -93,6 +101,11 @@ export const findSearchBasedSymbolDefinitions = async (props: FindRelatedSymbols
} }
}, },
languageFilter, languageFilter,
...(repoName ? [{
repo: {
regexp: `^${escapeStringRegexp(repoName)}$`,
}
}]: [])
] ]
} }
} }

View file

@ -4,7 +4,16 @@ import { rangeSchema, repositoryInfoSchema } from "../search/types";
export const findRelatedSymbolsRequestSchema = z.object({ export const findRelatedSymbolsRequestSchema = z.object({
symbolName: z.string(), symbolName: z.string(),
language: z.string(), language: z.string(),
/**
* Optional revision name to scope search to.
* If not provided, the search will be scoped to HEAD.
*/
revisionName: z.string().optional(), revisionName: z.string().optional(),
/**
* Optional repository name to scope search to.
* If not provided, the search will be across all repositories.
*/
repoName: z.string().optional(),
}); });
export type FindRelatedSymbolsRequest = z.infer<typeof findRelatedSymbolsRequestSchema>; export type FindRelatedSymbolsRequest = z.infer<typeof findRelatedSymbolsRequestSchema>;

View file

@ -6,6 +6,7 @@ import { search } from "./searchApi";
import { sew } from "@/actions"; import { sew } from "@/actions";
import { withOptionalAuthV2 } from "@/withAuthV2"; import { withOptionalAuthV2 } from "@/withAuthV2";
import { QueryIR } from './ir'; import { QueryIR } from './ir';
import escapeStringRegexp from "escape-string-regexp";
// @todo (bkellam) #574 : We should really be using `git show <hash>:<path>` to fetch file contents here. // @todo (bkellam) #574 : We should really be using `git show <hash>:<path>` to fetch file contents here.
// This will allow us to support permalinks to files at a specific revision that may not be indexed // This will allow us to support permalinks to files at a specific revision that may not be indexed
@ -18,7 +19,7 @@ export const getFileSource = async ({ fileName, repository, branch }: FileSource
children: [ children: [
{ {
repo: { repo: {
regexp: `^${repository}$`, regexp: `^${escapeStringRegexp(repository)}$`,
}, },
}, },
{ {