diff --git a/CHANGELOG.md b/CHANGELOG.md index 52970136..0a45d299 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 filtering panel for filtering results by repository and by language. ([#48](https://github.com/sourcebot-dev/sourcebot/pull/48)) + ## [2.1.1] - 2024-10-25 ### Fixed diff --git a/packages/web/package.json b/packages/web/package.json index ae7cb054..cc523f8e 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -45,6 +45,7 @@ "embla-carousel-auto-scroll": "^8.3.0", "embla-carousel-react": "^8.3.0", "escape-string-regexp": "^5.0.0", + "fuse.js": "^7.0.0", "http-status-codes": "^2.3.0", "lucide-react": "^0.435.0", "next": "14.2.10", diff --git a/packages/web/public/languages/file_type_assembly.svg b/packages/web/public/languages/file_type_assembly.svg new file mode 100644 index 00000000..4c4584b5 --- /dev/null +++ b/packages/web/public/languages/file_type_assembly.svg @@ -0,0 +1 @@ +file_type_assembly \ No newline at end of file diff --git a/packages/web/public/languages/file_type_c3.svg b/packages/web/public/languages/file_type_c3.svg new file mode 100644 index 00000000..2d8ac394 --- /dev/null +++ b/packages/web/public/languages/file_type_c3.svg @@ -0,0 +1 @@ +file_type_c3 \ No newline at end of file diff --git a/packages/web/public/languages/file_type_cpp3.svg b/packages/web/public/languages/file_type_cpp3.svg new file mode 100644 index 00000000..b999f2ea --- /dev/null +++ b/packages/web/public/languages/file_type_cpp3.svg @@ -0,0 +1 @@ +file_type_cpp3 \ No newline at end of file diff --git a/packages/web/public/languages/file_type_csharp2.svg b/packages/web/public/languages/file_type_csharp2.svg new file mode 100644 index 00000000..882bf4a3 --- /dev/null +++ b/packages/web/public/languages/file_type_csharp2.svg @@ -0,0 +1 @@ +file_type_csharp2 \ No newline at end of file diff --git a/packages/web/public/languages/file_type_css.svg b/packages/web/public/languages/file_type_css.svg new file mode 100644 index 00000000..3d8959a0 --- /dev/null +++ b/packages/web/public/languages/file_type_css.svg @@ -0,0 +1 @@ +file_type_css \ No newline at end of file diff --git a/packages/web/public/languages/file_type_dartlang.svg b/packages/web/public/languages/file_type_dartlang.svg new file mode 100644 index 00000000..0b258097 --- /dev/null +++ b/packages/web/public/languages/file_type_dartlang.svg @@ -0,0 +1 @@ +file_type_dartlang \ No newline at end of file diff --git a/packages/web/public/languages/file_type_go.svg b/packages/web/public/languages/file_type_go.svg new file mode 100644 index 00000000..05a1baa1 --- /dev/null +++ b/packages/web/public/languages/file_type_go.svg @@ -0,0 +1 @@ +file_type_go \ No newline at end of file diff --git a/packages/web/public/languages/file_type_haskell.svg b/packages/web/public/languages/file_type_haskell.svg new file mode 100644 index 00000000..f818dac4 --- /dev/null +++ b/packages/web/public/languages/file_type_haskell.svg @@ -0,0 +1 @@ +file_type_haskell \ No newline at end of file diff --git a/packages/web/public/languages/file_type_html.svg b/packages/web/public/languages/file_type_html.svg new file mode 100644 index 00000000..a0152d86 --- /dev/null +++ b/packages/web/public/languages/file_type_html.svg @@ -0,0 +1 @@ +file_type_html \ No newline at end of file diff --git a/packages/web/public/languages/file_type_java.svg b/packages/web/public/languages/file_type_java.svg new file mode 100644 index 00000000..14121c58 --- /dev/null +++ b/packages/web/public/languages/file_type_java.svg @@ -0,0 +1 @@ +file_type_java \ No newline at end of file diff --git a/packages/web/public/languages/file_type_js_official.svg b/packages/web/public/languages/file_type_js_official.svg new file mode 100644 index 00000000..bcfade41 --- /dev/null +++ b/packages/web/public/languages/file_type_js_official.svg @@ -0,0 +1 @@ +file_type_js_official \ No newline at end of file diff --git a/packages/web/public/languages/file_type_json.svg b/packages/web/public/languages/file_type_json.svg new file mode 100644 index 00000000..26c39ba7 --- /dev/null +++ b/packages/web/public/languages/file_type_json.svg @@ -0,0 +1 @@ +file_type_json \ No newline at end of file diff --git a/packages/web/public/languages/file_type_julia.svg b/packages/web/public/languages/file_type_julia.svg new file mode 100644 index 00000000..49343a27 --- /dev/null +++ b/packages/web/public/languages/file_type_julia.svg @@ -0,0 +1 @@ +file_type_julia \ No newline at end of file diff --git a/packages/web/public/languages/file_type_kotlin.svg b/packages/web/public/languages/file_type_kotlin.svg new file mode 100644 index 00000000..4b0961cb --- /dev/null +++ b/packages/web/public/languages/file_type_kotlin.svg @@ -0,0 +1 @@ +file_type_kotlin \ No newline at end of file diff --git a/packages/web/public/languages/file_type_lua.svg b/packages/web/public/languages/file_type_lua.svg new file mode 100644 index 00000000..44f3fa08 --- /dev/null +++ b/packages/web/public/languages/file_type_lua.svg @@ -0,0 +1 @@ +file_type_lua \ No newline at end of file diff --git a/packages/web/public/languages/file_type_markdown.svg b/packages/web/public/languages/file_type_markdown.svg new file mode 100644 index 00000000..c5b32a6f --- /dev/null +++ b/packages/web/public/languages/file_type_markdown.svg @@ -0,0 +1 @@ +file_type_markdown \ No newline at end of file diff --git a/packages/web/public/languages/file_type_matlab.svg b/packages/web/public/languages/file_type_matlab.svg new file mode 100644 index 00000000..0b5e3755 --- /dev/null +++ b/packages/web/public/languages/file_type_matlab.svg @@ -0,0 +1 @@ +file_type_matlab \ No newline at end of file diff --git a/packages/web/public/languages/file_type_objectivec.svg b/packages/web/public/languages/file_type_objectivec.svg new file mode 100644 index 00000000..fe0a61be --- /dev/null +++ b/packages/web/public/languages/file_type_objectivec.svg @@ -0,0 +1 @@ +file_type_objectivec \ No newline at end of file diff --git a/packages/web/public/languages/file_type_ocaml.svg b/packages/web/public/languages/file_type_ocaml.svg new file mode 100644 index 00000000..8e5d8e9a --- /dev/null +++ b/packages/web/public/languages/file_type_ocaml.svg @@ -0,0 +1 @@ +file_type_ocaml \ No newline at end of file diff --git a/packages/web/public/languages/file_type_perl.svg b/packages/web/public/languages/file_type_perl.svg new file mode 100644 index 00000000..8b8be680 --- /dev/null +++ b/packages/web/public/languages/file_type_perl.svg @@ -0,0 +1 @@ +file_type_perl \ No newline at end of file diff --git a/packages/web/public/languages/file_type_php3.svg b/packages/web/public/languages/file_type_php3.svg new file mode 100644 index 00000000..aaed635e --- /dev/null +++ b/packages/web/public/languages/file_type_php3.svg @@ -0,0 +1 @@ +file_type_php3 \ No newline at end of file diff --git a/packages/web/public/languages/file_type_powershell.svg b/packages/web/public/languages/file_type_powershell.svg new file mode 100644 index 00000000..05c95b31 --- /dev/null +++ b/packages/web/public/languages/file_type_powershell.svg @@ -0,0 +1 @@ +file_type_powershell \ No newline at end of file diff --git a/packages/web/public/languages/file_type_python.svg b/packages/web/public/languages/file_type_python.svg new file mode 100644 index 00000000..677f2165 --- /dev/null +++ b/packages/web/public/languages/file_type_python.svg @@ -0,0 +1 @@ +file_type_python \ No newline at end of file diff --git a/packages/web/public/languages/file_type_r.svg b/packages/web/public/languages/file_type_r.svg new file mode 100644 index 00000000..28f49c5e --- /dev/null +++ b/packages/web/public/languages/file_type_r.svg @@ -0,0 +1 @@ +file_type_r \ No newline at end of file diff --git a/packages/web/public/languages/file_type_ruby.svg b/packages/web/public/languages/file_type_ruby.svg new file mode 100644 index 00000000..9443db1f --- /dev/null +++ b/packages/web/public/languages/file_type_ruby.svg @@ -0,0 +1 @@ +file_type_ruby \ No newline at end of file diff --git a/packages/web/public/languages/file_type_rust.svg b/packages/web/public/languages/file_type_rust.svg new file mode 100644 index 00000000..327fd299 --- /dev/null +++ b/packages/web/public/languages/file_type_rust.svg @@ -0,0 +1 @@ +file_type_rust \ No newline at end of file diff --git a/packages/web/public/languages/file_type_shell.svg b/packages/web/public/languages/file_type_shell.svg new file mode 100644 index 00000000..17d38213 --- /dev/null +++ b/packages/web/public/languages/file_type_shell.svg @@ -0,0 +1 @@ +file_type_shell \ No newline at end of file diff --git a/packages/web/public/languages/file_type_swift.svg b/packages/web/public/languages/file_type_swift.svg new file mode 100644 index 00000000..c232d1f7 --- /dev/null +++ b/packages/web/public/languages/file_type_swift.svg @@ -0,0 +1 @@ +file_type_swift \ No newline at end of file diff --git a/packages/web/public/languages/file_type_tex.svg b/packages/web/public/languages/file_type_tex.svg new file mode 100644 index 00000000..952a2dec --- /dev/null +++ b/packages/web/public/languages/file_type_tex.svg @@ -0,0 +1 @@ +file_type_tex \ No newline at end of file diff --git a/packages/web/public/languages/file_type_text.svg b/packages/web/public/languages/file_type_text.svg new file mode 100644 index 00000000..a5562edd --- /dev/null +++ b/packages/web/public/languages/file_type_text.svg @@ -0,0 +1 @@ +file_type_text \ No newline at end of file diff --git a/packages/web/public/languages/file_type_typescript_official.svg b/packages/web/public/languages/file_type_typescript_official.svg new file mode 100644 index 00000000..bac7e33c --- /dev/null +++ b/packages/web/public/languages/file_type_typescript_official.svg @@ -0,0 +1 @@ +file_type_typescript_official \ No newline at end of file diff --git a/packages/web/public/languages/file_type_yaml.svg b/packages/web/public/languages/file_type_yaml.svg new file mode 100644 index 00000000..601979d5 --- /dev/null +++ b/packages/web/public/languages/file_type_yaml.svg @@ -0,0 +1 @@ +file_type_yaml \ No newline at end of file diff --git a/packages/web/public/languages/file_type_zig.svg b/packages/web/public/languages/file_type_zig.svg new file mode 100644 index 00000000..7e954652 --- /dev/null +++ b/packages/web/public/languages/file_type_zig.svg @@ -0,0 +1 @@ +file_type_zig \ No newline at end of file diff --git a/packages/web/src/app/globals.css b/packages/web/src/app/globals.css index 7153c1ee..d1ff0cbb 100644 --- a/packages/web/src/app/globals.css +++ b/packages/web/src/app/globals.css @@ -85,4 +85,12 @@ .cm-editor .cm-searchMatch-selected { border: solid; +} + +.truncate-start { + direction: rtl; + text-align: left; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } \ No newline at end of file diff --git a/packages/web/src/app/repositoryCarousel.tsx b/packages/web/src/app/repositoryCarousel.tsx index f67aea0f..0969ff81 100644 --- a/packages/web/src/app/repositoryCarousel.tsx +++ b/packages/web/src/app/repositoryCarousel.tsx @@ -64,7 +64,7 @@ const RepositoryBadge = ({ repoIcon: , repoName: info.repoName, repoLink: info.repoLink, diff --git a/packages/web/src/app/search/components/codePreviewPanel/codePreview.tsx b/packages/web/src/app/search/components/codePreviewPanel/codePreview.tsx index 35f764da..3aa6fb34 100644 --- a/packages/web/src/app/search/components/codePreviewPanel/codePreview.tsx +++ b/packages/web/src/app/search/components/codePreviewPanel/codePreview.tsx @@ -43,8 +43,8 @@ export const CodePreview = ({ }: CodePreviewProps) => { const editorRef = useRef(null); - const [ keymapType ] = useKeymapType(); - const { theme } = useThemeNormalized(); + const [keymapType] = useKeymapType(); + const { theme } = useThemeNormalized(); const [gutterWidth, setGutterWidth] = useState(0); const keymapExtension = useExtensionWithDependency( @@ -109,7 +109,9 @@ export const CodePreview = ({ return ( - + + + {/* Gutter icon */} + + + {/* File path */} + { if (file?.link) { window.open(file.link, "_blank"); } }} + title={file?.filepath} > {file?.filepath} - + + + {/* Match selector */} {file && file.matches.length > 0 && ( <> {`${selectedMatchIndex + 1} of ${ranges.length}`} @@ -154,6 +163,8 @@ export const CodePreview = ({ > )} + + {/* Close button */} void +} + +export const Entry = ({ + entry: { + isSelected, + icon, + iconAltText, + iconClassName, + displayName, + count, + }, + onClicked, +}: EntryProps) => { + + return ( + onClicked()} + > + + {icon ? ( + + ) : ( + + )} + {displayName} + + {count} + + ); +} + +export const compareEntries = (a: Entry, b: Entry) => { + if (a.isSelected !== b.isSelected) { + return a.isSelected ? 1 : -1; + } + + return a.count - b.count; +} \ No newline at end of file diff --git a/packages/web/src/app/search/components/filterPanel/filter.tsx b/packages/web/src/app/search/components/filterPanel/filter.tsx new file mode 100644 index 00000000..0faacbca --- /dev/null +++ b/packages/web/src/app/search/components/filterPanel/filter.tsx @@ -0,0 +1,66 @@ +'use client'; + +import { useMemo, useState } from "react"; +import { compareEntries, Entry } from "./entry"; +import { Input } from "@/components/ui/input"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import Fuse from "fuse.js"; + +interface FilterProps { + title: string, + searchPlaceholder: string, + entries: Entry[], + onEntryClicked: (key: string) => void, +} + +export const Filter = ({ + title, + searchPlaceholder, + entries, + onEntryClicked, +}: FilterProps) => { + const [searchFilter, setSearchFilter] = useState(""); + + const filteredEntries = useMemo(() => { + if (searchFilter === "") { + return entries; + } + + const fuse = new Fuse(entries, { + keys: ["displayName"], + threshold: 0.3, + }); + + const result = fuse.search(searchFilter); + return result.map((result) => result.item); + }, [entries, searchFilter]); + + return ( + + {title} + setSearchFilter(event.target.value)} + /> + + + + {filteredEntries + .sort((entryA, entryB) => compareEntries(entryB, entryA)) + .map((entry) => ( + onEntryClicked(entry.key)} + /> + ))} + + + + ) +} \ No newline at end of file diff --git a/packages/web/src/app/search/components/filterPanel/index.tsx b/packages/web/src/app/search/components/filterPanel/index.tsx new file mode 100644 index 00000000..1a220fe3 --- /dev/null +++ b/packages/web/src/app/search/components/filterPanel/index.tsx @@ -0,0 +1,147 @@ +'use client'; + +import { SearchResultFile } from "@/lib/types"; +import { getRepoCodeHostInfo } from "@/lib/utils"; +import { SetStateAction, useCallback, useEffect, useState } from "react"; +import { Entry } from "./entry"; +import { Filter } from "./filter"; +import { getLanguageIcon } from "./languageIcons"; + +interface FilePanelProps { + matches: SearchResultFile[]; + onFilterChanged: (filteredMatches: SearchResultFile[]) => void, +} + +export const FilterPanel = ({ + matches, + onFilterChanged, +}: FilePanelProps) => { + const [repos, setRepos] = useState>({}); + const [languages, setLanguages] = useState>({}); + + useEffect(() => { + const _repos = aggregateMatches( + "Repository", + matches, + (key) => { + const info = getRepoCodeHostInfo(key); + return { + key, + displayName: info?.repoName ?? key, + count: 0, + isSelected: false, + icon: info?.icon, + iconAltText: info?.costHostName, + iconClassName: info?.iconClassName, + }; + } + ); + + setRepos(_repos); + }, [matches, setRepos]); + + useEffect(() => { + const _languages = aggregateMatches( + "Language", + matches, + (key) => { + // @todo: Get language icons + return { + key, + displayName: key, + count: 0, + isSelected: false, + icon: getLanguageIcon(key), + } satisfies Entry; + } + ) + + setLanguages(_languages); + }, [matches, setLanguages]); + + const onEntryClicked = useCallback(( + key: string, + setter: (value: SetStateAction>) => void, + ) => { + setter((values) => ({ + ...values, + [key]: { + ...values[key], + isSelected: !values[key].isSelected, + }, + })); + }, []); + + useEffect(() => { + const selectedRepos = new Set( + Object.entries(repos) + .filter(([_, { isSelected }]) => isSelected) + .map(([key]) => key) + ); + + const selectedLanguages = new Set( + Object.entries(languages) + .filter(([_, { isSelected }]) => isSelected) + .map(([key]) => key) + ); + + const filteredMatches = matches.filter((match) => + ( + (selectedRepos.size === 0 ? true : selectedRepos.has(match.Repository)) && + (selectedLanguages.size === 0 ? true : selectedLanguages.has(match.Language)) + ) + ); + + onFilterChanged(filteredMatches); + }, [matches, repos, languages]); + + return ( + + Filter Results + + onEntryClicked(key, setRepos)} + /> + + onEntryClicked(key, setLanguages)} + /> + + ) +} + +/* Aggregates `matches` by the given `propName`. The result is a record + * of `Entry` objects, where the key is the aggregated `propName` and + * the value is the entry created by `createEntry`. Example: + * + * "repo1": { + * "count": 22, + * ... + * }, + * "repo2": { + * "count": 9, + * ... + * } + */ +const aggregateMatches = ( + propName: 'Repository' | 'Language', + matches: SearchResultFile[], + createEntry: (key: string) => Entry +) => { + return matches + .map((match) => match[propName]) + .filter((key) => key.length > 0) + .reduce((aggregation, key) => { + if (!aggregation[key]) { + aggregation[key] = createEntry(key); + } + aggregation[key].count += 1; + return aggregation; + }, {} as Record) +} \ No newline at end of file diff --git a/packages/web/src/app/search/components/filterPanel/languageIcons.ts b/packages/web/src/app/search/components/filterPanel/languageIcons.ts new file mode 100644 index 00000000..c9e2156f --- /dev/null +++ b/packages/web/src/app/search/components/filterPanel/languageIcons.ts @@ -0,0 +1,110 @@ +import JavaScriptIcon from "@/public/languages/file_type_js_official.svg"; +import TypeScriptIcon from "@/public/languages/file_type_typescript_official.svg"; +import GoIcon from "@/public/languages/file_type_go.svg"; +import MarkdownIcon from "@/public/languages/file_type_markdown.svg"; +import CIcon from "@/public/languages/file_type_c3.svg"; +import CppIcon from "@/public/languages/file_type_cpp3.svg"; +import CSharpIcon from "@/public/languages/file_type_csharp2.svg"; +import CSSIcon from "@/public/languages/file_type_css.svg"; +import HTMLIcon from "@/public/languages/file_type_html.svg"; +import JavaIcon from "@/public/languages/file_type_java.svg"; +import JSONIcon from "@/public/languages/file_type_json.svg"; +import PythonIcon from "@/public/languages/file_type_python.svg"; +import RubyIcon from "@/public/languages/file_type_ruby.svg"; +import RustIcon from "@/public/languages/file_type_rust.svg"; +import YAMLIcon from "@/public/languages/file_type_yaml.svg"; +import KotlinIcon from "@/public/languages/file_type_kotlin.svg"; +import SwiftIcon from "@/public/languages/file_type_swift.svg"; +import PHPIcon from "@/public/languages/file_type_php3.svg"; +import RIcon from "@/public/languages/file_type_r.svg"; +import MatlabIcon from "@/public/languages/file_type_matlab.svg"; +import ObjectiveCIcon from "@/public/languages/file_type_objectivec.svg"; +import LuaIcon from "@/public/languages/file_type_lua.svg"; +import DartIcon from "@/public/languages/file_type_dartlang.svg"; +import HaskellIcon from "@/public/languages/file_type_haskell.svg"; +import PerlIcon from "@/public/languages/file_type_perl.svg"; +import ShellIcon from "@/public/languages/file_type_shell.svg"; +import ZigIcon from "@/public/languages/file_type_zig.svg"; +import JuliaIcon from "@/public/languages/file_type_julia.svg"; +import OcamlIcon from "@/public/languages/file_type_ocaml.svg"; +import TextIcon from "@/public/languages/file_type_text.svg"; +import PowershellIcon from "@/public/languages/file_type_powershell.svg"; +import TexIcon from "@/public/languages/file_type_tex.svg"; +import AssemblyIcon from "@/public/languages/file_type_assembly.svg"; + +export const getLanguageIcon = (language: string) => { + switch (language.toLowerCase()) { + case "tsx": + case "typescript": + return TypeScriptIcon; + case "jsx": + case "javascript": + return JavaScriptIcon; + case "go": + return GoIcon; + case "markdown": + return MarkdownIcon; + case "c": + return CIcon; + case "c++": + return CppIcon; + case "python": + return PythonIcon; + case "c#": + return CSharpIcon; + case "html": + return HTMLIcon; + case "css": + return CSSIcon; + case "java": + return JavaIcon; + case "json with comments": + case "json": + return JSONIcon; + case "ruby": + return RubyIcon; + case "rust": + return RustIcon; + case "yaml": + return YAMLIcon; + case "kotlin": + return KotlinIcon; + case "swift": + return SwiftIcon; + case "php": + return PHPIcon; + case "r": + return RIcon; + case "matlab": + return MatlabIcon; + case "objective-c": + return ObjectiveCIcon; + case "lua": + return LuaIcon; + case "dart": + return DartIcon; + case "haskell": + return HaskellIcon; + case "perl": + return PerlIcon; + case "makefile": + case "shell": + return ShellIcon; + case "zig": + return ZigIcon; + case "julia": + return JuliaIcon; + case "ocaml": + return OcamlIcon; + case "text": + return TextIcon; + case "powershell": + return PowershellIcon; + case "tex": + return TexIcon; + case "assembly": + return AssemblyIcon; + default: + return null; + } +} diff --git a/packages/web/src/app/search/components/searchResultsPanel/fileMatchContainer.tsx b/packages/web/src/app/search/components/searchResultsPanel/fileMatchContainer.tsx index 05cb3dff..02951f49 100644 --- a/packages/web/src/app/search/components/searchResultsPanel/fileMatchContainer.tsx +++ b/packages/web/src/app/search/components/searchResultsPanel/fileMatchContainer.tsx @@ -63,7 +63,7 @@ export const FileMatchContainer = ({ repoIcon: } } @@ -95,12 +95,12 @@ export const FileMatchContainer = ({ return ( { onOpenFile(); }} > - + {repoIcon} ยท - {!fileNameRange ? ( - {file.FileName} - ) : ( - - {file.FileName.slice(0, fileNameRange.from)} - - {file.FileName.slice(fileNameRange.from, fileNameRange.to)} - - {file.FileName.slice(fileNameRange.to)} + + + {!fileNameRange ? + file.FileName + : ( + <> + {file.FileName.slice(0, fileNameRange.from)} + + {file.FileName.slice(fileNameRange.from, fileNameRange.to)} + + {file.FileName.slice(fileNameRange.to)} + > + )} - )} + {matches.map((match, index) => ( diff --git a/packages/web/src/app/search/page.tsx b/packages/web/src/app/search/page.tsx index 005d15ed..09316ac3 100644 --- a/packages/web/src/app/search/page.tsx +++ b/packages/web/src/app/search/page.tsx @@ -5,25 +5,27 @@ import { ResizablePanel, ResizablePanelGroup, } from "@/components/ui/resizable"; +import { ScrollArea } from "@/components/ui/scroll-area"; import { Separator } from "@/components/ui/separator"; +import useCaptureEvent from "@/hooks/useCaptureEvent"; import { useNonEmptyQueryParam } from "@/hooks/useNonEmptyQueryParam"; +import { SearchQueryParams, SearchResultFile } from "@/lib/types"; import { createPathWithQueryParams } from "@/lib/utils"; import { SymbolIcon } from "@radix-ui/react-icons"; +import { Scrollbar } from "@radix-ui/react-scroll-area"; import { useQuery } from "@tanstack/react-query"; import Image from "next/image"; import { useRouter } from "next/navigation"; -import { useCallback, useEffect, useMemo, useState } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import logoDark from "../../../public/sb_logo_dark.png"; import logoLight from "../../../public/sb_logo_light.png"; import { search } from "../api/(client)/client"; import { SearchBar } from "../searchBar"; import { SettingsDropdown } from "../settingsDropdown"; -import useCaptureEvent from "@/hooks/useCaptureEvent"; import { CodePreviewPanel } from "./components/codePreviewPanel"; +import { FilterPanel } from "./components/filterPanel"; import { SearchResultsPanel } from "./components/searchResultsPanel"; -import { SearchQueryParams, SearchResultFile } from "@/lib/types"; -import { ScrollArea } from "@/components/ui/scroll-area"; -import { Scrollbar } from "@radix-ui/react-scroll-area"; +import { ImperativePanelHandle } from "react-resizable-panels"; const DEFAULT_MAX_MATCH_DISPLAY_COUNT = 200; @@ -33,9 +35,6 @@ export default function SearchPage() { const _maxMatchDisplayCount = parseInt(useNonEmptyQueryParam(SearchQueryParams.maxMatchDisplayCount) ?? `${DEFAULT_MAX_MATCH_DISPLAY_COUNT}`); const maxMatchDisplayCount = isNaN(_maxMatchDisplayCount) ? DEFAULT_MAX_MATCH_DISPLAY_COUNT : _maxMatchDisplayCount; - const [selectedMatchIndex, setSelectedMatchIndex] = useState(0); - const [selectedFile, setSelectedFile] = useState(undefined); - const captureEvent = useCaptureEvent(); const { data: searchResponse, isLoading } = useQuery({ @@ -151,80 +150,148 @@ export default function SearchPage() { /> - - { - isLoading ? ( - Loading... - ) : fileMatches.length > 0 && searchResponse ? ( - {`[${searchDurationMs} ms] Displaying ${numMatches} of ${searchResponse.Result.MatchCount} matches in ${fileMatches.length} ${fileMatches.length > 1 ? 'files' : 'file'}`} - ) : ( - No results - ) - } - {isMoreResultsButtonVisible && !isLoading && ( - - (load more) - - )} - + {!isLoading && ( + + { + fileMatches.length > 0 && searchResponse ? ( + {`[${searchDurationMs} ms] Displaying ${numMatches} of ${searchResponse.Result.MatchCount} matches in ${fileMatches.length} ${fileMatches.length > 1 ? 'files' : 'file'}`} + ) : ( + No results + ) + } + {isMoreResultsButtonVisible && ( + + (load more) + + )} + + )} - {/* Search Results & Code Preview */} - - - {isLoading ? ( - - - Searching... - - ) : fileMatches.length > 0 ? ( - - { - setSelectedFile(fileMatch); - }} - onMatchIndexChanged={(matchIndex) => { - setSelectedMatchIndex(matchIndex); - }} - /> - {isMoreResultsButtonVisible && ( - - - Load more results - - - )} - - - ) : ( - - No results found - - )} - - - - setSelectedFile(undefined)} - selectedMatchIndex={selectedMatchIndex} - onSelectedMatchIndexChange={setSelectedMatchIndex} - /> - - + {isLoading ? ( + + + Searching... + + ) : ( + + )} ); } + +interface PanelGroupProps { + fileMatches: SearchResultFile[]; + isMoreResultsButtonVisible?: boolean; + onLoadMoreResults: () => void; +} + +const PanelGroup = ({ + fileMatches, + isMoreResultsButtonVisible, + onLoadMoreResults, +}: PanelGroupProps) => { + const [selectedMatchIndex, setSelectedMatchIndex] = useState(0); + const [selectedFile, setSelectedFile] = useState(undefined); + const [filteredFileMatches, setFilteredFileMatches] = useState(fileMatches); + + const codePreviewPanelRef = useRef(null); + useEffect(() => { + if (selectedFile) { + codePreviewPanelRef.current?.expand(); + } else { + codePreviewPanelRef.current?.collapse(); + } + }, [selectedFile]); + + return ( + + {/* ~~ Filter panel ~~ */} + + { + setFilteredFileMatches(filteredFileMatches) + }} + /> + + + + {/* ~~ Search results ~~ */} + + {filteredFileMatches.length > 0 ? ( + + { + setSelectedFile(fileMatch); + }} + onMatchIndexChanged={(matchIndex) => { + setSelectedMatchIndex(matchIndex); + }} + /> + {isMoreResultsButtonVisible && ( + + + Load more results + + + )} + + + ) : ( + + No results found + + )} + + + + {/* ~~ Code preview ~~ */} + + setSelectedFile(undefined)} + selectedMatchIndex={selectedMatchIndex} + onSelectedMatchIndexChange={setSelectedMatchIndex} + /> + + + ) +} \ No newline at end of file diff --git a/packages/web/src/lib/utils.ts b/packages/web/src/lib/utils.ts index c04def13..38846d5f 100644 --- a/packages/web/src/lib/utils.ts +++ b/packages/web/src/lib/utils.ts @@ -35,7 +35,7 @@ type CodeHostInfo = { costHostName: string; repoLink: string; icon: string; - iconClassname?: string; + iconClassName?: string; } export const getRepoCodeHostInfo = (repoName: string): CodeHostInfo | undefined => { @@ -46,7 +46,7 @@ export const getRepoCodeHostInfo = (repoName: string): CodeHostInfo | undefined costHostName: "GitHub", repoLink: `https://${repoName}`, icon: githubLogo, - iconClassname: "dark:invert", + iconClassName: "dark:invert", } } diff --git a/packages/web/tsconfig.json b/packages/web/tsconfig.json index 7b285893..6b2f2e65 100644 --- a/packages/web/tsconfig.json +++ b/packages/web/tsconfig.json @@ -18,7 +18,8 @@ } ], "paths": { - "@/*": ["./src/*"] + "@/*": ["./src/*"], + "@/public/*": ["./public/*"] } }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], diff --git a/yarn.lock b/yarn.lock index df394cff..6ffb2bc5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2739,6 +2739,11 @@ functions-have-names@^1.2.3: resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834" integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== +fuse.js@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/fuse.js/-/fuse.js-7.0.0.tgz#6573c9fcd4c8268e403b4fc7d7131ffcf99a9eb2" + integrity sha512-14F4hBIxqKvD4Zz/XjDc3y94mNZN6pRv3U13Udo0lNLCWRBUsrMv2xwcF/y/Z5sV6+FQW+/ow68cHpm4sunt8Q== + get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.2, get-intrinsic@^1.2.3, get-intrinsic@^1.2.4: version "1.2.4" resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd"
{`${selectedMatchIndex + 1} of ${ranges.length}`}
{displayName}
{count}
Loading...
{`[${searchDurationMs} ms] Displaying ${numMatches} of ${searchResponse.Result.MatchCount} matches in ${fileMatches.length} ${fileMatches.length > 1 ? 'files' : 'file'}`}
No results
Searching...
No results found