Dark theme improvements (#226)

This commit is contained in:
Brendan Kellam 2025-03-07 09:23:55 -08:00 committed by GitHub
parent e82c5e454e
commit 2286d94eba
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 200 additions and 110 deletions

View file

@ -4,11 +4,11 @@ import { ScrollArea } from "@/components/ui/scroll-area";
import { useKeymapExtension } from "@/hooks/useKeymapExtension"; import { useKeymapExtension } from "@/hooks/useKeymapExtension";
import { useNonEmptyQueryParam } from "@/hooks/useNonEmptyQueryParam"; import { useNonEmptyQueryParam } from "@/hooks/useNonEmptyQueryParam";
import { useSyntaxHighlightingExtension } from "@/hooks/useSyntaxHighlightingExtension"; import { useSyntaxHighlightingExtension } from "@/hooks/useSyntaxHighlightingExtension";
import { useThemeNormalized } from "@/hooks/useThemeNormalized";
import { search } from "@codemirror/search"; import { search } from "@codemirror/search";
import CodeMirror, { Decoration, DecorationSet, EditorSelection, EditorView, ReactCodeMirrorRef, SelectionRange, StateField, ViewUpdate } from "@uiw/react-codemirror"; import CodeMirror, { Decoration, DecorationSet, EditorSelection, EditorView, ReactCodeMirrorRef, SelectionRange, StateField, ViewUpdate } from "@uiw/react-codemirror";
import { useEffect, useMemo, useRef, useState } from "react"; import { useEffect, useMemo, useRef, useState } from "react";
import { EditorContextMenu } from "../../components/editorContextMenu"; import { EditorContextMenu } from "../../components/editorContextMenu";
import { useCodeMirrorTheme } from "@/hooks/useCodeMirrorTheme";
interface CodePreviewProps { interface CodePreviewProps {
path: string; path: string;
@ -119,7 +119,7 @@ export const CodePreview = ({
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [highlightRange, isEditorCreated]); }, [highlightRange, isEditorCreated]);
const { theme } = useThemeNormalized(); const theme = useCodeMirrorTheme();
return ( return (
<ScrollArea className="h-full overflow-auto flex-1"> <ScrollArea className="h-full overflow-auto flex-1">
@ -132,7 +132,7 @@ export const CodePreview = ({
value={source} value={source}
extensions={extensions} extensions={extensions}
readOnly={true} readOnly={true}
theme={theme === "dark" ? "dark" : "light"} theme={theme}
> >
{editorRef.current && editorRef.current.view && currentSelection && ( {editorRef.current && editorRef.current.view && currentSelection && (
<EditorContextMenu <EditorContextMenu

View file

@ -1,4 +1,4 @@
import { FileHeader } from "@/app/[domain]/components/fireHeader"; import { FileHeader } from "@/app/[domain]/components/fileHeader";
import { TopBar } from "@/app/[domain]/components/topBar"; import { TopBar } from "@/app/[domain]/components/topBar";
import { Separator } from '@/components/ui/separator'; import { Separator } from '@/components/ui/separator';
import { getFileSource, listRepositories } from '@/lib/server/searchService'; import { getFileSource, listRepositories } from '@/lib/server/searchService';

View file

@ -2,7 +2,6 @@
import { ScrollArea } from "@/components/ui/scroll-area"; import { ScrollArea } from "@/components/ui/scroll-area";
import { useKeymapExtension } from "@/hooks/useKeymapExtension"; import { useKeymapExtension } from "@/hooks/useKeymapExtension";
import { useThemeNormalized } from "@/hooks/useThemeNormalized";
import { json, jsonLanguage, jsonParseLinter } from "@codemirror/lang-json"; import { json, jsonLanguage, jsonParseLinter } from "@codemirror/lang-json";
import { linter } from "@codemirror/lint"; import { linter } from "@codemirror/lint";
import { EditorView, hoverTooltip } from "@codemirror/view"; import { EditorView, hoverTooltip } from "@codemirror/view";
@ -14,6 +13,7 @@ import { Schema } from "ajv";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import useCaptureEvent from "@/hooks/useCaptureEvent"; import useCaptureEvent from "@/hooks/useCaptureEvent";
import { CodeHostType } from "@/lib/utils"; import { CodeHostType } from "@/lib/utils";
import { useCodeMirrorTheme } from "@/hooks/useCodeMirrorTheme";
export type QuickActionFn<T> = (previous: T) => T; export type QuickActionFn<T> = (previous: T) => T;
export type QuickAction<T> = { export type QuickAction<T> = {
@ -119,7 +119,7 @@ const ConfigEditor = <T,>(props: ConfigEditorProps<T>, forwardedRef: Ref<ReactCo
); );
const keymapExtension = useKeymapExtension(editorRef.current?.view); const keymapExtension = useKeymapExtension(editorRef.current?.view);
const { theme } = useThemeNormalized(); const theme = useCodeMirrorTheme();
// ⚠️ DISGUSTING HACK AHEAD ⚠️ // ⚠️ DISGUSTING HACK AHEAD ⚠️
// Background: When navigating to the /connections/:id?tab=settings page, we were hitting a 500 error with the following // Background: When navigating to the /connections/:id?tab=settings page, we were hitting a 500 error with the following
@ -251,7 +251,7 @@ const ConfigEditor = <T,>(props: ConfigEditorProps<T>, forwardedRef: Ref<ReactCo
customAutocompleteStyle, customAutocompleteStyle,
...jsonSchemaExtensions, ...jsonSchemaExtensions,
]} ]}
theme={theme === "dark" ? "dark" : "light"} theme={theme}
/> />
</ScrollArea> </ScrollArea>
<div <div

View file

@ -23,7 +23,6 @@ export const FileHeader = ({
branchDisplayName, branchDisplayName,
branchDisplayTitle, branchDisplayTitle,
}: FileHeaderProps) => { }: FileHeaderProps) => {
const info = getRepoCodeHostInfo(repo); const info = getRepoCodeHostInfo(repo);
return ( return (
@ -47,7 +46,7 @@ export const FileHeader = ({
</Link> </Link>
{branchDisplayName && ( {branchDisplayName && (
<p <p
className="text-xs font-semibold text-gray-500 dark:text-gray-400 mt-0.5 flex items-center gap-0.5" className="text-xs font-semibold text-gray-500 dark:text-gray-400 mt-[3px] flex items-center gap-0.5"
title={branchDisplayTitle} title={branchDisplayTitle}
> >
{/* hack since to make the @ symbol look more centered with the text */} {/* hack since to make the @ symbol look more centered with the text */}
@ -64,7 +63,9 @@ export const FileHeader = ({
</p> </p>
)} )}
<span>·</span> <span>·</span>
<div className="flex-1 flex items-center overflow-hidden"> <div
className="flex-1 flex items-center overflow-hidden mt-0.5"
>
<span className="inline-block w-full truncate-start font-mono text-sm"> <span className="inline-block w-full truncate-start font-mono text-sm">
{!fileNameHighlightRange ? {!fileNameHighlightRange ?
fileName fileName

View file

@ -3,9 +3,9 @@
import { EditorContextMenu } from "@/app/[domain]/components/editorContextMenu"; import { EditorContextMenu } from "@/app/[domain]/components/editorContextMenu";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { ScrollArea } from "@/components/ui/scroll-area"; import { ScrollArea } from "@/components/ui/scroll-area";
import { useCodeMirrorTheme } from "@/hooks/useCodeMirrorTheme";
import { useKeymapExtension } from "@/hooks/useKeymapExtension"; import { useKeymapExtension } from "@/hooks/useKeymapExtension";
import { useSyntaxHighlightingExtension } from "@/hooks/useSyntaxHighlightingExtension"; import { useSyntaxHighlightingExtension } from "@/hooks/useSyntaxHighlightingExtension";
import { useThemeNormalized } from "@/hooks/useThemeNormalized";
import { gutterWidthExtension } from "@/lib/extensions/gutterWidthExtension"; import { gutterWidthExtension } from "@/lib/extensions/gutterWidthExtension";
import { highlightRanges, searchResultHighlightExtension } from "@/lib/extensions/searchResultHighlightExtension"; import { highlightRanges, searchResultHighlightExtension } from "@/lib/extensions/searchResultHighlightExtension";
import { SearchResultFileMatch } from "@/lib/types"; import { SearchResultFileMatch } from "@/lib/types";
@ -44,8 +44,8 @@ export const CodePreview = ({
}: CodePreviewProps) => { }: CodePreviewProps) => {
const editorRef = useRef<ReactCodeMirrorRef>(null); const editorRef = useRef<ReactCodeMirrorRef>(null);
const { theme } = useThemeNormalized();
const [gutterWidth, setGutterWidth] = useState(0); const [gutterWidth, setGutterWidth] = useState(0);
const theme = useCodeMirrorTheme();
const keymapExtension = useKeymapExtension(editorRef.current?.view); const keymapExtension = useKeymapExtension(editorRef.current?.view);
const syntaxHighlighting = useSyntaxHighlightingExtension(file?.language ?? '', editorRef.current?.view); const syntaxHighlighting = useSyntaxHighlightingExtension(file?.language ?? '', editorRef.current?.view);
@ -106,7 +106,7 @@ export const CodePreview = ({
return ( return (
<div className="flex flex-col h-full"> <div className="flex flex-col h-full">
<div className="flex flex-row bg-cyan-200 dark:bg-cyan-900 items-center justify-between pr-3 py-0.5"> <div className="flex flex-row bg-accent items-center justify-between pr-3 py-0.5 mt-7">
{/* Gutter icon */} {/* Gutter icon */}
<div className="flex flex-row"> <div className="flex flex-row">
@ -178,8 +178,8 @@ export const CodePreview = ({
className="relative" className="relative"
readOnly={true} readOnly={true}
value={file?.content} value={file?.content}
theme={theme === "dark" ? "dark" : "light"}
extensions={extensions} extensions={extensions}
theme={theme}
> >
{ {
editorRef.current?.view && editorRef.current?.view &&

View file

@ -6,7 +6,7 @@ import { useQuery } from "@tanstack/react-query";
import { CodePreview, CodePreviewFile } from "./codePreview"; import { CodePreview, CodePreviewFile } from "./codePreview";
import { SearchResultFile } from "@/lib/types"; import { SearchResultFile } from "@/lib/types";
import { useDomain } from "@/hooks/useDomain"; import { useDomain } from "@/hooks/useDomain";
import { SymbolIcon } from "@radix-ui/react-icons";
interface CodePreviewPanelProps { interface CodePreviewPanelProps {
fileMatch?: SearchResultFile; fileMatch?: SearchResultFile;
onClose: () => void; onClose: () => void;
@ -24,7 +24,7 @@ export const CodePreviewPanel = ({
}: CodePreviewPanelProps) => { }: CodePreviewPanelProps) => {
const domain = useDomain(); const domain = useDomain();
const { data: file } = useQuery({ const { data: file, isLoading } = useQuery({
queryKey: ["source", fileMatch?.FileName, fileMatch?.Repository, fileMatch?.Branches], queryKey: ["source", fileMatch?.FileName, fileMatch?.Repository, fileMatch?.Branches],
queryFn: async (): Promise<CodePreviewFile | undefined> => { queryFn: async (): Promise<CodePreviewFile | undefined> => {
if (!fileMatch) { if (!fileMatch) {
@ -88,6 +88,13 @@ export const CodePreviewPanel = ({
enabled: fileMatch !== undefined, enabled: fileMatch !== undefined,
}); });
if (isLoading) {
return <div className="flex flex-col items-center justify-center h-full">
<SymbolIcon className="h-6 w-6 animate-spin" />
<p className="font-semibold text-center">Loading...</p>
</div>
}
return ( return (
<CodePreview <CodePreview
file={file} file={file}

View file

@ -46,7 +46,7 @@ export const Entry = ({
)} )}
<p className="overflow-hidden text-ellipsis whitespace-nowrap">{displayName}</p> <p className="overflow-hidden text-ellipsis whitespace-nowrap">{displayName}</p>
</div> </div>
<div className="px-2 py-0.5 bg-gray-100 dark:bg-gray-800 text-sm rounded-md"> <div className="px-2 py-0.5 bg-accent text-sm rounded-md">
{countText} {countText}
</div> </div>
</div> </div>

View file

@ -3,13 +3,11 @@
import { getCodemirrorLanguage } from "@/lib/codemirrorLanguage"; import { getCodemirrorLanguage } from "@/lib/codemirrorLanguage";
import { lineOffsetExtension } from "@/lib/extensions/lineOffsetExtension"; import { lineOffsetExtension } from "@/lib/extensions/lineOffsetExtension";
import { SearchResultRange } from "@/lib/types"; import { SearchResultRange } from "@/lib/types";
import { defaultHighlightStyle, syntaxHighlighting } from "@codemirror/language";
import { EditorState, StateField, Transaction } from "@codemirror/state"; import { EditorState, StateField, Transaction } from "@codemirror/state";
import { defaultLightThemeOption, oneDarkHighlightStyle, oneDarkTheme } from "@uiw/react-codemirror";
import { Decoration, DecorationSet, EditorView, lineNumbers } from "@codemirror/view"; import { Decoration, DecorationSet, EditorView, lineNumbers } from "@codemirror/view";
import { useMemo, useRef } from "react"; import { useMemo, useRef } from "react";
import { LightweightCodeMirror, CodeMirrorRef } from "./lightweightCodeMirror"; import { LightweightCodeMirror, CodeMirrorRef } from "./lightweightCodeMirror";
import { useThemeNormalized } from "@/hooks/useThemeNormalized"; import { useCodeMirrorTheme } from "@/hooks/useCodeMirrorTheme";
const markDecoration = Decoration.mark({ const markDecoration = Decoration.mark({
class: "cm-searchMatch-selected" class: "cm-searchMatch-selected"
@ -29,19 +27,13 @@ export const CodePreview = ({
lineOffset, lineOffset,
}: CodePreviewProps) => { }: CodePreviewProps) => {
const editorRef = useRef<CodeMirrorRef>(null); const editorRef = useRef<CodeMirrorRef>(null);
const { theme } = useThemeNormalized(); const theme = useCodeMirrorTheme();
const extensions = useMemo(() => { const extensions = useMemo(() => {
const codemirrorExtension = getCodemirrorLanguage(language); const codemirrorExtension = getCodemirrorLanguage(language);
return [ return [
EditorView.editable.of(false), EditorView.editable.of(false),
...(theme === 'dark' ? [ theme,
syntaxHighlighting(oneDarkHighlightStyle),
oneDarkTheme,
] : [
syntaxHighlighting(defaultHighlightStyle),
defaultLightThemeOption,
]),
lineNumbers(), lineNumbers(),
lineOffsetExtension(lineOffset), lineOffsetExtension(lineOffset),
codemirrorExtension ? codemirrorExtension : [], codemirrorExtension ? codemirrorExtension : [],

View file

@ -1,6 +1,6 @@
'use client'; 'use client';
import { FileHeader } from "@/app/[domain]/components/fireHeader"; import { FileHeader } from "@/app/[domain]/components/fileHeader";
import { Separator } from "@/components/ui/separator"; import { Separator } from "@/components/ui/separator";
import { Repository, SearchResultFile } from "@/lib/types"; import { Repository, SearchResultFile } from "@/lib/types";
import { DoubleArrowDownIcon, DoubleArrowUpIcon } from "@radix-ui/react-icons"; import { DoubleArrowDownIcon, DoubleArrowUpIcon } from "@radix-ui/react-icons";
@ -17,6 +17,7 @@ interface FileMatchContainerProps {
onShowAllMatchesButtonClicked: () => void; onShowAllMatchesButtonClicked: () => void;
isBranchFilteringEnabled: boolean; isBranchFilteringEnabled: boolean;
repoMetadata: Record<string, Repository>; repoMetadata: Record<string, Repository>;
yOffset: number;
} }
export const FileMatchContainer = ({ export const FileMatchContainer = ({
@ -27,6 +28,7 @@ export const FileMatchContainer = ({
onShowAllMatchesButtonClicked, onShowAllMatchesButtonClicked,
isBranchFilteringEnabled, isBranchFilteringEnabled,
repoMetadata, repoMetadata,
yOffset,
}: FileMatchContainerProps) => { }: FileMatchContainerProps) => {
const matchCount = useMemo(() => { const matchCount = useMemo(() => {
@ -92,7 +94,10 @@ export const FileMatchContainer = ({
<div> <div>
{/* Title */} {/* Title */}
<div <div
className="top-0 bg-cyan-200 dark:bg-cyan-900 primary-foreground px-2 py-0.5 flex flex-row items-center justify-between cursor-pointer" className="bg-accent primary-foreground px-2 py-0.5 flex flex-row items-center justify-between cursor-pointer sticky top-0 z-10"
style={{
top: `-${yOffset}px`,
}}
onClick={() => { onClick={() => {
onOpenFile(); onOpenFile();
}} }}
@ -119,7 +124,7 @@ export const FileMatchContainer = ({
}} }}
/> />
{(index !== matches.length - 1 || isMoreContentButtonVisible) && ( {(index !== matches.length - 1 || isMoreContentButtonVisible) && (
<Separator className="dark:bg-gray-400" /> <Separator className="bg-accent" />
)} )}
</div> </div>
))} ))}

View file

@ -124,36 +124,40 @@ export const SearchResultsPanel = ({
position: "relative", position: "relative",
}} }}
> >
{virtualizer.getVirtualItems().map((virtualRow) => ( {virtualizer.getVirtualItems().map((virtualRow) => {
<div const file = fileMatches[virtualRow.index];
key={virtualRow.key} return (
data-index={virtualRow.index} <div
ref={virtualizer.measureElement} key={virtualRow.key}
style={{ data-index={virtualRow.index}
position: 'absolute', ref={virtualizer.measureElement}
top: 0, style={{
left: 0, position: 'absolute',
width: '100%', transform: `translateY(${virtualRow.start}px)`,
transform: `translateY(${virtualRow.start}px)`, top: 0,
}} left: 0,
> width: '100%',
<FileMatchContainer
file={fileMatches[virtualRow.index]}
onOpenFile={() => {
onOpenFileMatch(fileMatches[virtualRow.index]);
}} }}
onMatchIndexChanged={(matchIndex) => { >
onMatchIndexChanged(matchIndex); <FileMatchContainer
}} file={file}
showAllMatches={showAllMatchesStates[virtualRow.index]} onOpenFile={() => {
onShowAllMatchesButtonClicked={() => { onOpenFileMatch(file);
onShowAllMatchesButtonClicked(virtualRow.index); }}
}} onMatchIndexChanged={(matchIndex) => {
isBranchFilteringEnabled={isBranchFilteringEnabled} onMatchIndexChanged(matchIndex);
repoMetadata={repoMetadata} }}
/> showAllMatches={showAllMatchesStates[virtualRow.index]}
</div> onShowAllMatchesButtonClicked={() => {
))} onShowAllMatchesButtonClicked(virtualRow.index);
}}
isBranchFilteringEnabled={isBranchFilteringEnabled}
repoMetadata={repoMetadata}
yOffset={virtualRow.start}
/>
</div>
)
})}
</div> </div>
{isLoadMoreButtonVisible && ( {isLoadMoreButtonVisible && (
<div className="p-3"> <div className="p-3">

View file

@ -2,7 +2,7 @@
import { EditorState, Extension, StateEffect } from "@codemirror/state"; import { EditorState, Extension, StateEffect } from "@codemirror/state";
import { EditorView } from "@codemirror/view"; import { EditorView } from "@codemirror/view";
import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from "react"; import { forwardRef, useEffect, useImperativeHandle, useRef } from "react";
interface CodeMirrorProps { interface CodeMirrorProps {
value?: string; value?: string;
@ -29,14 +29,14 @@ const LightweightCodeMirror = forwardRef<CodeMirrorRef, CodeMirrorProps>(({
className, className,
}, ref) => { }, ref) => {
const editor = useRef<HTMLDivElement | null>(null); const editor = useRef<HTMLDivElement | null>(null);
const [view, setView] = useState<EditorView>(); const viewRef = useRef<EditorView>();
const [state, setState] = useState<EditorState>(); const stateRef = useRef<EditorState>();
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
editor: editor.current, editor: editor.current,
state, state: stateRef.current,
view, view: viewRef.current,
}), [editor, state, view]); }), []);
useEffect(() => { useEffect(() => {
if (!editor.current) { if (!editor.current) {
@ -47,31 +47,26 @@ const LightweightCodeMirror = forwardRef<CodeMirrorRef, CodeMirrorProps>(({
extensions: [], /* extensions are explicitly left out here */ extensions: [], /* extensions are explicitly left out here */
doc: value, doc: value,
}); });
setState(state); stateRef.current = state;
const view = new EditorView({ const view = new EditorView({
state, state,
parent: editor.current, parent: editor.current,
}); });
setView(view); viewRef.current = view;
// console.debug(`[CM] Editor created.`);
return () => { return () => {
view.destroy(); view.destroy();
setView(undefined); viewRef.current = undefined;
setState(undefined); stateRef.current = undefined;
// console.debug(`[CM] Editor destroyed.`);
} }
}, [value]); }, [value]);
useEffect(() => { useEffect(() => {
if (view) { if (viewRef.current) {
view.dispatch({ effects: StateEffect.reconfigure.of(extensions ?? []) }); viewRef.current.dispatch({ effects: StateEffect.reconfigure.of(extensions ?? []) });
// console.debug(`[CM] Editor reconfigured.`);
} }
}, [extensions, view]); }, [extensions]);
return ( return (
<div <div

View file

@ -10,8 +10,8 @@ import useCaptureEvent from "@/hooks/useCaptureEvent";
import { useNonEmptyQueryParam } from "@/hooks/useNonEmptyQueryParam"; import { useNonEmptyQueryParam } from "@/hooks/useNonEmptyQueryParam";
import { useSearchHistory } from "@/hooks/useSearchHistory"; import { useSearchHistory } from "@/hooks/useSearchHistory";
import { Repository, SearchQueryParams, SearchResultFile } from "@/lib/types"; import { Repository, SearchQueryParams, SearchResultFile } from "@/lib/types";
import { createPathWithQueryParams } from "@/lib/utils"; import { createPathWithQueryParams, measure } from "@/lib/utils";
import { SymbolIcon } from "@radix-ui/react-icons"; import { InfoCircledIcon, SymbolIcon } from "@radix-ui/react-icons";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { Suspense, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Suspense, useCallback, useEffect, useMemo, useRef, useState } from "react";
@ -47,14 +47,19 @@ const SearchPageInternal = () => {
const { data: searchResponse, isLoading } = useQuery({ const { data: searchResponse, isLoading } = useQuery({
queryKey: ["search", searchQuery, maxMatchDisplayCount], queryKey: ["search", searchQuery, maxMatchDisplayCount],
queryFn: () => search({ queryFn: () => measure(() => search({
query: searchQuery, query: searchQuery,
maxMatchDisplayCount, maxMatchDisplayCount,
}, domain), }, domain), "client.search"),
select: ({ data, durationMs }) => ({
...data,
durationMs,
}),
enabled: searchQuery.length > 0, enabled: searchQuery.length > 0,
refetchOnWindowFocus: false, refetchOnWindowFocus: false,
}); });
// Write the query to the search history // Write the query to the search history
useEffect(() => { useEffect(() => {
if (searchQuery.length === 0) { if (searchQuery.length === 0) {
@ -136,7 +141,7 @@ const SearchPageInternal = () => {
return { return {
fileMatches: searchResponse.Result.Files ?? [], fileMatches: searchResponse.Result.Files ?? [],
searchDurationMs: Math.round(searchResponse.Result.Duration / 1000000), searchDurationMs: Math.round(searchResponse.durationMs),
totalMatchCount: searchResponse.Result.MatchCount, totalMatchCount: searchResponse.Result.MatchCount,
isBranchFilteringEnabled: searchResponse.isBranchFilteringEnabled, isBranchFilteringEnabled: searchResponse.isBranchFilteringEnabled,
repoUrlTemplates: searchResponse.Result.RepoURLs, repoUrlTemplates: searchResponse.Result.RepoURLs,
@ -160,12 +165,12 @@ const SearchPageInternal = () => {
}, [fileMatches]); }, [fileMatches]);
const onLoadMoreResults = useCallback(() => { const onLoadMoreResults = useCallback(() => {
const url = createPathWithQueryParams('/search', const url = createPathWithQueryParams(`/${domain}/search`,
[SearchQueryParams.query, searchQuery], [SearchQueryParams.query, searchQuery],
[SearchQueryParams.maxMatchDisplayCount, `${maxMatchDisplayCount * 2}`], [SearchQueryParams.maxMatchDisplayCount, `${maxMatchDisplayCount * 2}`],
) )
router.push(url); router.push(url);
}, [maxMatchDisplayCount, router, searchQuery]); }, [maxMatchDisplayCount, router, searchQuery, domain]);
return ( return (
<div className="flex flex-col h-screen overflow-clip"> <div className="flex flex-col h-screen overflow-clip">
@ -176,26 +181,6 @@ const SearchPageInternal = () => {
domain={domain} domain={domain}
/> />
<Separator /> <Separator />
{!isLoading && (
<div className="bg-accent py-1 px-2 flex flex-row items-center gap-4">
{
fileMatches.length > 0 ? (
<p className="text-sm font-medium">{`[${searchDurationMs} ms] Found ${numMatches} matches in ${fileMatches.length} ${fileMatches.length > 1 ? 'files' : 'file'}`}</p>
) : (
<p className="text-sm font-medium">No results</p>
)
}
{isMoreResultsButtonVisible && (
<div
className="cursor-pointer text-blue-500 text-sm hover:underline"
onClick={onLoadMoreResults}
>
(load more)
</div>
)}
</div>
)}
<Separator />
</div> </div>
{isLoading ? ( {isLoading ? (
@ -211,6 +196,8 @@ const SearchPageInternal = () => {
isBranchFilteringEnabled={isBranchFilteringEnabled} isBranchFilteringEnabled={isBranchFilteringEnabled}
repoUrlTemplates={repoUrlTemplates} repoUrlTemplates={repoUrlTemplates}
repoMetadata={repoMetadata ?? {}} repoMetadata={repoMetadata ?? {}}
searchDurationMs={searchDurationMs}
numMatches={numMatches}
/> />
)} )}
</div> </div>
@ -224,6 +211,8 @@ interface PanelGroupProps {
isBranchFilteringEnabled: boolean; isBranchFilteringEnabled: boolean;
repoUrlTemplates: Record<string, string>; repoUrlTemplates: Record<string, string>;
repoMetadata: Record<string, Repository>; repoMetadata: Record<string, Repository>;
searchDurationMs: number;
numMatches: number;
} }
const PanelGroup = ({ const PanelGroup = ({
@ -233,6 +222,8 @@ const PanelGroup = ({
isBranchFilteringEnabled, isBranchFilteringEnabled,
repoUrlTemplates, repoUrlTemplates,
repoMetadata, repoMetadata,
searchDurationMs,
numMatches,
}: PanelGroupProps) => { }: PanelGroupProps) => {
const [selectedMatchIndex, setSelectedMatchIndex] = useState(0); const [selectedMatchIndex, setSelectedMatchIndex] = useState(0);
const [selectedFile, setSelectedFile] = useState<SearchResultFile | undefined>(undefined); const [selectedFile, setSelectedFile] = useState<SearchResultFile | undefined>(undefined);
@ -272,7 +263,7 @@ const PanelGroup = ({
/> />
</ResizablePanel> </ResizablePanel>
<ResizableHandle <ResizableHandle
className="bg-accent w-1 transition-colors delay-50 data-[resize-handle-state=drag]:bg-accent-foreground data-[resize-handle-state=hover]:bg-accent-foreground" className="w-[1px] bg-accent transition-colors delay-50 data-[resize-handle-state=drag]:bg-accent-foreground data-[resize-handle-state=hover]:bg-accent-foreground"
/> />
{/* ~~ Search results ~~ */} {/* ~~ Search results ~~ */}
@ -281,6 +272,24 @@ const PanelGroup = ({
id={'search-results-panel'} id={'search-results-panel'}
order={2} order={2}
> >
<div className="py-1 px-2 flex flex-row items-center">
<InfoCircledIcon className="w-4 h-4 mr-2" />
{
fileMatches.length > 0 ? (
<p className="text-sm font-medium">{`[${searchDurationMs} ms] Found ${numMatches} matches in ${fileMatches.length} ${fileMatches.length > 1 ? 'files' : 'file'}`}</p>
) : (
<p className="text-sm font-medium">No results</p>
)
}
{isMoreResultsButtonVisible && (
<div
className="cursor-pointer text-blue-500 text-sm hover:underline ml-4"
onClick={onLoadMoreResults}
>
(load more)
</div>
)}
</div>
{filteredFileMatches.length > 0 ? ( {filteredFileMatches.length > 0 ? (
<SearchResultsPanel <SearchResultsPanel
fileMatches={filteredFileMatches} fileMatches={filteredFileMatches}
@ -302,7 +311,7 @@ const PanelGroup = ({
)} )}
</ResizablePanel> </ResizablePanel>
<ResizableHandle <ResizableHandle
withHandle={selectedFile !== undefined} className="mt-7 w-[1px] bg-accent transition-colors delay-50 data-[resize-handle-state=drag]:bg-accent-foreground data-[resize-handle-state=hover]:bg-accent-foreground"
/> />
{/* ~~ Code preview ~~ */} {/* ~~ Code preview ~~ */}

View file

@ -3,19 +3,18 @@
import { NEXT_PUBLIC_DOMAIN_SUB_PATH } from "@/lib/environment.client"; import { NEXT_PUBLIC_DOMAIN_SUB_PATH } from "@/lib/environment.client";
import { fileSourceResponseSchema, listRepositoriesResponseSchema, searchResponseSchema } from "@/lib/schemas"; import { fileSourceResponseSchema, listRepositoriesResponseSchema, searchResponseSchema } from "@/lib/schemas";
import { FileSourceRequest, FileSourceResponse, ListRepositoriesResponse, SearchRequest, SearchResponse } from "@/lib/types"; import { FileSourceRequest, FileSourceResponse, ListRepositoriesResponse, SearchRequest, SearchResponse } from "@/lib/types";
import { measure } from "@/lib/utils";
import assert from "assert"; import assert from "assert";
export const search = async (body: SearchRequest, domain: string): Promise<SearchResponse> => { export const search = async (body: SearchRequest, domain: string): Promise<SearchResponse> => {
const path = resolveServerPath("/api/search"); const path = resolveServerPath("/api/search");
const { data: result } = await measure(() => fetch(path, { const result = await fetch(path, {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
"X-Org-Domain": domain, "X-Org-Domain": domain,
}, },
body: JSON.stringify(body), body: JSON.stringify(body),
}).then(response => response.json()), "client.search"); }).then(response => response.json());
return searchResponseSchema.parse(result); return searchResponseSchema.parse(result);
} }

View file

@ -0,0 +1,78 @@
'use client';
import { useTailwind } from "./useTailwind";
import { useMemo } from "react";
import { useThemeNormalized } from "./useThemeNormalized";
import createTheme from "@uiw/codemirror-themes";
import { defaultLightThemeOption } from "@uiw/react-codemirror";
import { tags as t } from '@lezer/highlight';
import { syntaxHighlighting } from "@codemirror/language";
import { defaultHighlightStyle } from "@codemirror/language";
// From: https://github.com/codemirror/theme-one-dark/blob/main/src/one-dark.ts
const chalky = "#e5c07b",
coral = "#e06c75",
cyan = "#56b6c2",
invalid = "#ffffff",
ivory = "#abb2bf",
stone = "#7d8799",
malibu = "#61afef",
sage = "#98c379",
whiskey = "#d19a66",
violet = "#c678dd",
highlightBackground = "#2c313a",
background = "#282c34",
selection = "#3E4451",
cursor = "#528bff";
export const useCodeMirrorTheme = () => {
const tailwind = useTailwind();
const { theme } = useThemeNormalized();
const darkTheme = useMemo(() => {
return createTheme({
theme: 'dark',
settings: {
background: tailwind.theme.colors.background,
foreground: ivory,
caret: cursor,
selection: selection,
selectionMatch: "#aafe661a", // for matching selections
gutterBackground: background,
gutterForeground: stone,
gutterBorder: 'none',
gutterActiveForeground: ivory,
lineHighlight: highlightBackground,
},
styles: [
{ tag: t.comment, color: stone },
{ tag: t.keyword, color: violet },
{ tag: [t.name, t.deleted, t.character, t.propertyName, t.macroName], color: coral },
{ tag: [t.function(t.variableName), t.labelName], color: malibu },
{ tag: [t.color, t.constant(t.name), t.standard(t.name)], color: whiskey },
{ tag: [t.definition(t.name), t.separator], color: ivory },
{ tag: [t.typeName, t.className, t.number, t.changed, t.annotation, t.modifier, t.self, t.namespace], color: chalky },
{ tag: [t.operator, t.operatorKeyword, t.url, t.escape, t.regexp, t.link, t.special(t.string)], color: cyan },
{ tag: [t.meta], color: stone },
{ tag: t.strong, fontWeight: 'bold' },
{ tag: t.emphasis, fontStyle: 'italic' },
{ tag: t.strikethrough, textDecoration: 'line-through' },
{ tag: t.link, color: stone, textDecoration: 'underline' },
{ tag: t.heading, fontWeight: 'bold', color: coral },
{ tag: [t.atom, t.bool, t.special(t.variableName)], color: whiskey },
{ tag: [t.processingInstruction, t.string, t.inserted], color: sage },
{ tag: t.invalid, color: invalid }
]
});
}, []);
const cmTheme = useMemo(() => {
return theme === 'dark' ? darkTheme : [
defaultLightThemeOption,
syntaxHighlighting(defaultHighlightStyle),
]
}, [theme]);
return cmTheme;
}