diff --git a/CHANGELOG.md b/CHANGELOG.md index 7969af24..f0118e8d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Added file suggestions as a suggestion type. ([#88](https://github.com/sourcebot-dev/sourcebot/pull/88)) + ## [2.5.0] - 2024-11-22 ### Added diff --git a/packages/web/src/app/components/searchBar/searchBar.tsx b/packages/web/src/app/components/searchBar/searchBar.tsx index 7103f06e..49cb7dc9 100644 --- a/packages/web/src/app/components/searchBar/searchBar.tsx +++ b/packages/web/src/app/components/searchBar/searchBar.tsx @@ -1,7 +1,8 @@ 'use client'; +import { useClickListener } from "@/hooks/useClickListener"; import { useTailwind } from "@/hooks/useTailwind"; -import { Repository, SearchQueryParams } from "@/lib/types"; +import { SearchQueryParams } from "@/lib/types"; import { cn, createPathWithQueryParams } from "@/lib/utils"; import { cursorCharLeft, @@ -31,12 +32,10 @@ import { createTheme } from '@uiw/codemirror-themes'; import CodeMirror, { Annotation, EditorView, KeyBinding, keymap, ReactCodeMirrorRef } from "@uiw/react-codemirror"; import { cva } from "class-variance-authority"; import { useRouter } from "next/navigation"; -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { useCallback, useMemo, useRef, useState } from "react"; import { useHotkeys } from 'react-hotkeys-hook'; -import { SearchSuggestionsBox, Suggestion } from "./searchSuggestionsBox"; -import { useClickListener } from "@/hooks/useClickListener"; -import { getRepos } from "../../api/(client)/client"; -import languages from "./languages"; +import { SearchSuggestionsBox, SuggestionMode } from "./searchSuggestionsBox"; +import { useSuggestionsData } from "./useSuggestionsData"; import { zoekt } from "./zoektLanguageExtension"; interface SearchBarProps { @@ -97,6 +96,8 @@ export const SearchBar = ({ const focusEditor = useCallback(() => editorRef.current?.view?.focus(), []); const focusSuggestionsBox = useCallback(() => suggestionBoxRef.current?.focus(), []); + const [suggestionMode, setSuggestionMode] = useState("refine"); + const [suggestionQuery, setSuggestionQuery] = useState(""); const [_query, setQuery] = useState(defaultQuery ?? ""); const query = useMemo(() => { @@ -105,41 +106,10 @@ export const SearchBar = ({ return _query.replaceAll(/\n/g, " "); }, [_query]); - const [repos, setRepos] = useState([]); - useEffect(() => { - getRepos().then((response) => { - setRepos(response.List.Repos.map(r => r.Repository)); - }); - }, []); - - const suggestionData = useMemo(() => { - const repoSuggestions: Suggestion[] = repos.map((repo) => { - return { - value: repo.Name, - } - }); - - const languageSuggestions: Suggestion[] = languages.map((lang) => { - const spotlight = [ - "Python", - "Java", - "TypeScript", - "Go", - "C++", - "C#" - ].includes(lang); - - return { - value: lang, - spotlight, - }; - }) - - return { - repos: repoSuggestions, - languages: languageSuggestions, - } - }, [repos]); + const suggestionData = useSuggestionsData({ + suggestionMode, + suggestionQuery, + }); const theme = useMemo(() => { return createTheme({ @@ -286,6 +256,12 @@ export const SearchBar = ({ }} cursorPosition={cursorPosition} data={suggestionData} + onSuggestionModeChanged={(suggestionMode) => { + setSuggestionMode(suggestionMode); + }} + onSuggestionQueryChanged={(suggestionQuery) => { + setSuggestionQuery(suggestionQuery); + }} /> ) diff --git a/packages/web/src/app/components/searchBar/searchSuggestionsBox.tsx b/packages/web/src/app/components/searchBar/searchSuggestionsBox.tsx index 91fff3aa..e2cd0ecd 100644 --- a/packages/web/src/app/components/searchBar/searchSuggestionsBox.tsx +++ b/packages/web/src/app/components/searchBar/searchSuggestionsBox.tsx @@ -47,10 +47,13 @@ interface SearchSuggestionsBoxProps { onFocus: () => void; onBlur: () => void; onReturnFocus: () => void; + onSuggestionModeChanged: (suggestionMode: SuggestionMode) => void; + onSuggestionQueryChanged: (suggestionQuery: string) => void; data: { repos: Suggestion[]; languages: Suggestion[]; + files: Suggestion[]; } } @@ -64,6 +67,8 @@ const SearchSuggestionsBox = forwardRef(({ onFocus, onBlur, onReturnFocus, + onSuggestionModeChanged, + onSuggestionQueryChanged, }: SearchSuggestionsBoxProps, ref: Ref) => { const [highlightedSuggestionIndex, setHighlightedSuggestionIndex] = useState(0); @@ -137,6 +142,7 @@ const SearchSuggestionsBox = forwardRef(({ list, isHighlightEnabled = false, isSpotlightEnabled = false, + isClientSideSearchEnabled = true, onSuggestionClicked, Icon, } = ((): { @@ -145,6 +151,7 @@ const SearchSuggestionsBox = forwardRef(({ list: Suggestion[], isHighlightEnabled?: boolean, isSpotlightEnabled?: boolean, + isClientSideSearchEnabled?: boolean, onSuggestionClicked: (value: string) => void, Icon?: Icon } => { @@ -192,6 +199,11 @@ const SearchSuggestionsBox = forwardRef(({ onSuggestionClicked: createOnSuggestionClickedHandler({ trailingSpace: false }), } case "file": + return { + list: data.files, + onSuggestionClicked: createOnSuggestionClickedHandler(), + isClientSideSearchEnabled: false, + } case "revision": case "content": case "symbol": @@ -228,6 +240,10 @@ const SearchSuggestionsBox = forwardRef(({ return []; } + if (!isClientSideSearchEnabled) { + return list; + } + return fuse.search(suggestionQuery, { limit, }).map(result => result.item); @@ -240,13 +256,25 @@ const SearchSuggestionsBox = forwardRef(({ onSuggestionClicked, } - }, [suggestionQuery, suggestionMode, onCompletion, cursorPosition, data.repos, data.languages, query]); + }, [suggestionQuery, suggestionMode, query, cursorPosition, onCompletion, data.repos, data.files, data.languages]); // When the list of suggestions change, reset the highlight index useEffect(() => { setHighlightedSuggestionIndex(0); }, [suggestions]); + useEffect(() => { + if (isDefined(suggestionMode)) { + onSuggestionModeChanged(suggestionMode); + } + }, [onSuggestionModeChanged, suggestionMode]); + + useEffect(() => { + if (isDefined(suggestionQuery)) { + onSuggestionQueryChanged(suggestionQuery); + } + }, [onSuggestionQueryChanged, suggestionQuery]); + const suggestionModeText = useMemo(() => { if (!suggestionMode) { return ""; diff --git a/packages/web/src/app/components/searchBar/useSuggestionsData.ts b/packages/web/src/app/components/searchBar/useSuggestionsData.ts new file mode 100644 index 00000000..6ab9a82c --- /dev/null +++ b/packages/web/src/app/components/searchBar/useSuggestionsData.ts @@ -0,0 +1,75 @@ +'use client'; + +import { useQuery } from "@tanstack/react-query"; +import { Suggestion, SuggestionMode } from "./searchSuggestionsBox"; +import { getRepos, search } from "@/app/api/(client)/client"; +import { useMemo } from "react"; +import languages from "./languages"; + +interface Props { + suggestionMode: SuggestionMode; + suggestionQuery: string; +} + +/** + * Fetches suggestions for the search bar. + */ +export const useSuggestionsData = ({ + suggestionMode, + suggestionQuery, +}: Props) => { + const { data: repoSuggestions } = useQuery({ + queryKey: ["repoSuggestions"], + queryFn: getRepos, + select: (data): Suggestion[] => { + return data.List.Repos + .map(r => r.Repository) + .map(r => ({ + value: r.Name + })); + }, + enabled: suggestionMode === "repo", + }); + + const { data: fileSuggestions } = useQuery({ + queryKey: ["fileSuggestions", suggestionQuery], + queryFn: () => search({ + query: `file:${suggestionQuery}`, + maxMatchDisplayCount: 15, + }), + select: (data): Suggestion[] => { + return data.Result.Files?.map((file) => ({ + value: file.FileName + })) ?? []; + }, + enabled: suggestionMode === "file" + }); + + const languageSuggestions = useMemo((): Suggestion[] => { + return languages.map((lang) => { + const spotlight = [ + "Python", + "Java", + "TypeScript", + "Go", + "C++", + "C#" + ].includes(lang); + + return { + value: lang, + spotlight, + }; + }); + }, []); + + const data = useMemo(() => { + return { + repos: repoSuggestions ?? [], + languages: languageSuggestions, + files: fileSuggestions ?? [], + } + }, [repoSuggestions, fileSuggestions, languageSuggestions]); + + return data; +} \ No newline at end of file