mirror of
https://github.com/sourcebot-dev/sourcebot.git
synced 2025-12-12 12:25:22 +00:00
Dark theme improvements (#226)
This commit is contained in:
parent
e82c5e454e
commit
2286d94eba
14 changed files with 200 additions and 110 deletions
|
|
@ -4,11 +4,11 @@ import { ScrollArea } from "@/components/ui/scroll-area";
|
|||
import { useKeymapExtension } from "@/hooks/useKeymapExtension";
|
||||
import { useNonEmptyQueryParam } from "@/hooks/useNonEmptyQueryParam";
|
||||
import { useSyntaxHighlightingExtension } from "@/hooks/useSyntaxHighlightingExtension";
|
||||
import { useThemeNormalized } from "@/hooks/useThemeNormalized";
|
||||
import { search } from "@codemirror/search";
|
||||
import CodeMirror, { Decoration, DecorationSet, EditorSelection, EditorView, ReactCodeMirrorRef, SelectionRange, StateField, ViewUpdate } from "@uiw/react-codemirror";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { EditorContextMenu } from "../../components/editorContextMenu";
|
||||
import { useCodeMirrorTheme } from "@/hooks/useCodeMirrorTheme";
|
||||
|
||||
interface CodePreviewProps {
|
||||
path: string;
|
||||
|
|
@ -119,7 +119,7 @@ export const CodePreview = ({
|
|||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [highlightRange, isEditorCreated]);
|
||||
|
||||
const { theme } = useThemeNormalized();
|
||||
const theme = useCodeMirrorTheme();
|
||||
|
||||
return (
|
||||
<ScrollArea className="h-full overflow-auto flex-1">
|
||||
|
|
@ -132,7 +132,7 @@ export const CodePreview = ({
|
|||
value={source}
|
||||
extensions={extensions}
|
||||
readOnly={true}
|
||||
theme={theme === "dark" ? "dark" : "light"}
|
||||
theme={theme}
|
||||
>
|
||||
{editorRef.current && editorRef.current.view && currentSelection && (
|
||||
<EditorContextMenu
|
||||
|
|
|
|||
|
|
@ -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 { Separator } from '@/components/ui/separator';
|
||||
import { getFileSource, listRepositories } from '@/lib/server/searchService';
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { useKeymapExtension } from "@/hooks/useKeymapExtension";
|
||||
import { useThemeNormalized } from "@/hooks/useThemeNormalized";
|
||||
import { json, jsonLanguage, jsonParseLinter } from "@codemirror/lang-json";
|
||||
import { linter } from "@codemirror/lint";
|
||||
import { EditorView, hoverTooltip } from "@codemirror/view";
|
||||
|
|
@ -14,6 +13,7 @@ import { Schema } from "ajv";
|
|||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import useCaptureEvent from "@/hooks/useCaptureEvent";
|
||||
import { CodeHostType } from "@/lib/utils";
|
||||
import { useCodeMirrorTheme } from "@/hooks/useCodeMirrorTheme";
|
||||
|
||||
export type QuickActionFn<T> = (previous: T) => 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 { theme } = useThemeNormalized();
|
||||
const theme = useCodeMirrorTheme();
|
||||
|
||||
// ⚠️ DISGUSTING HACK AHEAD ⚠️
|
||||
// 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,
|
||||
...jsonSchemaExtensions,
|
||||
]}
|
||||
theme={theme === "dark" ? "dark" : "light"}
|
||||
theme={theme}
|
||||
/>
|
||||
</ScrollArea>
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@ export const FileHeader = ({
|
|||
branchDisplayName,
|
||||
branchDisplayTitle,
|
||||
}: FileHeaderProps) => {
|
||||
|
||||
const info = getRepoCodeHostInfo(repo);
|
||||
|
||||
return (
|
||||
|
|
@ -47,7 +46,7 @@ export const FileHeader = ({
|
|||
</Link>
|
||||
{branchDisplayName && (
|
||||
<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}
|
||||
>
|
||||
{/* hack since to make the @ symbol look more centered with the text */}
|
||||
|
|
@ -64,7 +63,9 @@ export const FileHeader = ({
|
|||
</p>
|
||||
)}
|
||||
<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">
|
||||
{!fileNameHighlightRange ?
|
||||
fileName
|
||||
|
|
@ -3,9 +3,9 @@
|
|||
import { EditorContextMenu } from "@/app/[domain]/components/editorContextMenu";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { useCodeMirrorTheme } from "@/hooks/useCodeMirrorTheme";
|
||||
import { useKeymapExtension } from "@/hooks/useKeymapExtension";
|
||||
import { useSyntaxHighlightingExtension } from "@/hooks/useSyntaxHighlightingExtension";
|
||||
import { useThemeNormalized } from "@/hooks/useThemeNormalized";
|
||||
import { gutterWidthExtension } from "@/lib/extensions/gutterWidthExtension";
|
||||
import { highlightRanges, searchResultHighlightExtension } from "@/lib/extensions/searchResultHighlightExtension";
|
||||
import { SearchResultFileMatch } from "@/lib/types";
|
||||
|
|
@ -44,8 +44,8 @@ export const CodePreview = ({
|
|||
}: CodePreviewProps) => {
|
||||
const editorRef = useRef<ReactCodeMirrorRef>(null);
|
||||
|
||||
const { theme } = useThemeNormalized();
|
||||
const [gutterWidth, setGutterWidth] = useState(0);
|
||||
const theme = useCodeMirrorTheme();
|
||||
|
||||
const keymapExtension = useKeymapExtension(editorRef.current?.view);
|
||||
const syntaxHighlighting = useSyntaxHighlightingExtension(file?.language ?? '', editorRef.current?.view);
|
||||
|
|
@ -106,7 +106,7 @@ export const CodePreview = ({
|
|||
|
||||
return (
|
||||
<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 */}
|
||||
<div className="flex flex-row">
|
||||
|
|
@ -178,8 +178,8 @@ export const CodePreview = ({
|
|||
className="relative"
|
||||
readOnly={true}
|
||||
value={file?.content}
|
||||
theme={theme === "dark" ? "dark" : "light"}
|
||||
extensions={extensions}
|
||||
theme={theme}
|
||||
>
|
||||
{
|
||||
editorRef.current?.view &&
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { useQuery } from "@tanstack/react-query";
|
|||
import { CodePreview, CodePreviewFile } from "./codePreview";
|
||||
import { SearchResultFile } from "@/lib/types";
|
||||
import { useDomain } from "@/hooks/useDomain";
|
||||
|
||||
import { SymbolIcon } from "@radix-ui/react-icons";
|
||||
interface CodePreviewPanelProps {
|
||||
fileMatch?: SearchResultFile;
|
||||
onClose: () => void;
|
||||
|
|
@ -24,7 +24,7 @@ export const CodePreviewPanel = ({
|
|||
}: CodePreviewPanelProps) => {
|
||||
const domain = useDomain();
|
||||
|
||||
const { data: file } = useQuery({
|
||||
const { data: file, isLoading } = useQuery({
|
||||
queryKey: ["source", fileMatch?.FileName, fileMatch?.Repository, fileMatch?.Branches],
|
||||
queryFn: async (): Promise<CodePreviewFile | undefined> => {
|
||||
if (!fileMatch) {
|
||||
|
|
@ -88,6 +88,13 @@ export const CodePreviewPanel = ({
|
|||
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 (
|
||||
<CodePreview
|
||||
file={file}
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ export const Entry = ({
|
|||
)}
|
||||
<p className="overflow-hidden text-ellipsis whitespace-nowrap">{displayName}</p>
|
||||
</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}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -3,13 +3,11 @@
|
|||
import { getCodemirrorLanguage } from "@/lib/codemirrorLanguage";
|
||||
import { lineOffsetExtension } from "@/lib/extensions/lineOffsetExtension";
|
||||
import { SearchResultRange } from "@/lib/types";
|
||||
import { defaultHighlightStyle, syntaxHighlighting } from "@codemirror/language";
|
||||
import { EditorState, StateField, Transaction } from "@codemirror/state";
|
||||
import { defaultLightThemeOption, oneDarkHighlightStyle, oneDarkTheme } from "@uiw/react-codemirror";
|
||||
import { Decoration, DecorationSet, EditorView, lineNumbers } from "@codemirror/view";
|
||||
import { useMemo, useRef } from "react";
|
||||
import { LightweightCodeMirror, CodeMirrorRef } from "./lightweightCodeMirror";
|
||||
import { useThemeNormalized } from "@/hooks/useThemeNormalized";
|
||||
import { useCodeMirrorTheme } from "@/hooks/useCodeMirrorTheme";
|
||||
|
||||
const markDecoration = Decoration.mark({
|
||||
class: "cm-searchMatch-selected"
|
||||
|
|
@ -29,19 +27,13 @@ export const CodePreview = ({
|
|||
lineOffset,
|
||||
}: CodePreviewProps) => {
|
||||
const editorRef = useRef<CodeMirrorRef>(null);
|
||||
const { theme } = useThemeNormalized();
|
||||
const theme = useCodeMirrorTheme();
|
||||
|
||||
const extensions = useMemo(() => {
|
||||
const codemirrorExtension = getCodemirrorLanguage(language);
|
||||
return [
|
||||
EditorView.editable.of(false),
|
||||
...(theme === 'dark' ? [
|
||||
syntaxHighlighting(oneDarkHighlightStyle),
|
||||
oneDarkTheme,
|
||||
] : [
|
||||
syntaxHighlighting(defaultHighlightStyle),
|
||||
defaultLightThemeOption,
|
||||
]),
|
||||
theme,
|
||||
lineNumbers(),
|
||||
lineOffsetExtension(lineOffset),
|
||||
codemirrorExtension ? codemirrorExtension : [],
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
'use client';
|
||||
|
||||
import { FileHeader } from "@/app/[domain]/components/fireHeader";
|
||||
import { FileHeader } from "@/app/[domain]/components/fileHeader";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Repository, SearchResultFile } from "@/lib/types";
|
||||
import { DoubleArrowDownIcon, DoubleArrowUpIcon } from "@radix-ui/react-icons";
|
||||
|
|
@ -17,6 +17,7 @@ interface FileMatchContainerProps {
|
|||
onShowAllMatchesButtonClicked: () => void;
|
||||
isBranchFilteringEnabled: boolean;
|
||||
repoMetadata: Record<string, Repository>;
|
||||
yOffset: number;
|
||||
}
|
||||
|
||||
export const FileMatchContainer = ({
|
||||
|
|
@ -27,6 +28,7 @@ export const FileMatchContainer = ({
|
|||
onShowAllMatchesButtonClicked,
|
||||
isBranchFilteringEnabled,
|
||||
repoMetadata,
|
||||
yOffset,
|
||||
}: FileMatchContainerProps) => {
|
||||
|
||||
const matchCount = useMemo(() => {
|
||||
|
|
@ -92,7 +94,10 @@ export const FileMatchContainer = ({
|
|||
<div>
|
||||
{/* Title */}
|
||||
<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={() => {
|
||||
onOpenFile();
|
||||
}}
|
||||
|
|
@ -119,7 +124,7 @@ export const FileMatchContainer = ({
|
|||
}}
|
||||
/>
|
||||
{(index !== matches.length - 1 || isMoreContentButtonVisible) && (
|
||||
<Separator className="dark:bg-gray-400" />
|
||||
<Separator className="bg-accent" />
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -124,36 +124,40 @@ export const SearchResultsPanel = ({
|
|||
position: "relative",
|
||||
}}
|
||||
>
|
||||
{virtualizer.getVirtualItems().map((virtualRow) => (
|
||||
<div
|
||||
key={virtualRow.key}
|
||||
data-index={virtualRow.index}
|
||||
ref={virtualizer.measureElement}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
transform: `translateY(${virtualRow.start}px)`,
|
||||
}}
|
||||
>
|
||||
<FileMatchContainer
|
||||
file={fileMatches[virtualRow.index]}
|
||||
onOpenFile={() => {
|
||||
onOpenFileMatch(fileMatches[virtualRow.index]);
|
||||
{virtualizer.getVirtualItems().map((virtualRow) => {
|
||||
const file = fileMatches[virtualRow.index];
|
||||
return (
|
||||
<div
|
||||
key={virtualRow.key}
|
||||
data-index={virtualRow.index}
|
||||
ref={virtualizer.measureElement}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
transform: `translateY(${virtualRow.start}px)`,
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
}}
|
||||
onMatchIndexChanged={(matchIndex) => {
|
||||
onMatchIndexChanged(matchIndex);
|
||||
}}
|
||||
showAllMatches={showAllMatchesStates[virtualRow.index]}
|
||||
onShowAllMatchesButtonClicked={() => {
|
||||
onShowAllMatchesButtonClicked(virtualRow.index);
|
||||
}}
|
||||
isBranchFilteringEnabled={isBranchFilteringEnabled}
|
||||
repoMetadata={repoMetadata}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
>
|
||||
<FileMatchContainer
|
||||
file={file}
|
||||
onOpenFile={() => {
|
||||
onOpenFileMatch(file);
|
||||
}}
|
||||
onMatchIndexChanged={(matchIndex) => {
|
||||
onMatchIndexChanged(matchIndex);
|
||||
}}
|
||||
showAllMatches={showAllMatchesStates[virtualRow.index]}
|
||||
onShowAllMatchesButtonClicked={() => {
|
||||
onShowAllMatchesButtonClicked(virtualRow.index);
|
||||
}}
|
||||
isBranchFilteringEnabled={isBranchFilteringEnabled}
|
||||
repoMetadata={repoMetadata}
|
||||
yOffset={virtualRow.start}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
{isLoadMoreButtonVisible && (
|
||||
<div className="p-3">
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import { EditorState, Extension, StateEffect } from "@codemirror/state";
|
||||
import { EditorView } from "@codemirror/view";
|
||||
import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from "react";
|
||||
import { forwardRef, useEffect, useImperativeHandle, useRef } from "react";
|
||||
|
||||
interface CodeMirrorProps {
|
||||
value?: string;
|
||||
|
|
@ -29,14 +29,14 @@ const LightweightCodeMirror = forwardRef<CodeMirrorRef, CodeMirrorProps>(({
|
|||
className,
|
||||
}, ref) => {
|
||||
const editor = useRef<HTMLDivElement | null>(null);
|
||||
const [view, setView] = useState<EditorView>();
|
||||
const [state, setState] = useState<EditorState>();
|
||||
const viewRef = useRef<EditorView>();
|
||||
const stateRef = useRef<EditorState>();
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
editor: editor.current,
|
||||
state,
|
||||
view,
|
||||
}), [editor, state, view]);
|
||||
state: stateRef.current,
|
||||
view: viewRef.current,
|
||||
}), []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor.current) {
|
||||
|
|
@ -47,31 +47,26 @@ const LightweightCodeMirror = forwardRef<CodeMirrorRef, CodeMirrorProps>(({
|
|||
extensions: [], /* extensions are explicitly left out here */
|
||||
doc: value,
|
||||
});
|
||||
setState(state);
|
||||
stateRef.current = state;
|
||||
|
||||
const view = new EditorView({
|
||||
state,
|
||||
parent: editor.current,
|
||||
});
|
||||
setView(view);
|
||||
|
||||
// console.debug(`[CM] Editor created.`);
|
||||
viewRef.current = view;
|
||||
|
||||
return () => {
|
||||
view.destroy();
|
||||
setView(undefined);
|
||||
setState(undefined);
|
||||
// console.debug(`[CM] Editor destroyed.`);
|
||||
viewRef.current = undefined;
|
||||
stateRef.current = undefined;
|
||||
}
|
||||
|
||||
}, [value]);
|
||||
|
||||
useEffect(() => {
|
||||
if (view) {
|
||||
view.dispatch({ effects: StateEffect.reconfigure.of(extensions ?? []) });
|
||||
// console.debug(`[CM] Editor reconfigured.`);
|
||||
if (viewRef.current) {
|
||||
viewRef.current.dispatch({ effects: StateEffect.reconfigure.of(extensions ?? []) });
|
||||
}
|
||||
}, [extensions, view]);
|
||||
}, [extensions]);
|
||||
|
||||
return (
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -10,8 +10,8 @@ import useCaptureEvent from "@/hooks/useCaptureEvent";
|
|||
import { useNonEmptyQueryParam } from "@/hooks/useNonEmptyQueryParam";
|
||||
import { useSearchHistory } from "@/hooks/useSearchHistory";
|
||||
import { Repository, SearchQueryParams, SearchResultFile } from "@/lib/types";
|
||||
import { createPathWithQueryParams } from "@/lib/utils";
|
||||
import { SymbolIcon } from "@radix-ui/react-icons";
|
||||
import { createPathWithQueryParams, measure } from "@/lib/utils";
|
||||
import { InfoCircledIcon, SymbolIcon } from "@radix-ui/react-icons";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Suspense, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
|
|
@ -47,14 +47,19 @@ const SearchPageInternal = () => {
|
|||
|
||||
const { data: searchResponse, isLoading } = useQuery({
|
||||
queryKey: ["search", searchQuery, maxMatchDisplayCount],
|
||||
queryFn: () => search({
|
||||
queryFn: () => measure(() => search({
|
||||
query: searchQuery,
|
||||
maxMatchDisplayCount,
|
||||
}, domain),
|
||||
}, domain), "client.search"),
|
||||
select: ({ data, durationMs }) => ({
|
||||
...data,
|
||||
durationMs,
|
||||
}),
|
||||
enabled: searchQuery.length > 0,
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
|
||||
|
||||
// Write the query to the search history
|
||||
useEffect(() => {
|
||||
if (searchQuery.length === 0) {
|
||||
|
|
@ -136,7 +141,7 @@ const SearchPageInternal = () => {
|
|||
|
||||
return {
|
||||
fileMatches: searchResponse.Result.Files ?? [],
|
||||
searchDurationMs: Math.round(searchResponse.Result.Duration / 1000000),
|
||||
searchDurationMs: Math.round(searchResponse.durationMs),
|
||||
totalMatchCount: searchResponse.Result.MatchCount,
|
||||
isBranchFilteringEnabled: searchResponse.isBranchFilteringEnabled,
|
||||
repoUrlTemplates: searchResponse.Result.RepoURLs,
|
||||
|
|
@ -160,12 +165,12 @@ const SearchPageInternal = () => {
|
|||
}, [fileMatches]);
|
||||
|
||||
const onLoadMoreResults = useCallback(() => {
|
||||
const url = createPathWithQueryParams('/search',
|
||||
const url = createPathWithQueryParams(`/${domain}/search`,
|
||||
[SearchQueryParams.query, searchQuery],
|
||||
[SearchQueryParams.maxMatchDisplayCount, `${maxMatchDisplayCount * 2}`],
|
||||
)
|
||||
router.push(url);
|
||||
}, [maxMatchDisplayCount, router, searchQuery]);
|
||||
}, [maxMatchDisplayCount, router, searchQuery, domain]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-screen overflow-clip">
|
||||
|
|
@ -176,26 +181,6 @@ const SearchPageInternal = () => {
|
|||
domain={domain}
|
||||
/>
|
||||
<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>
|
||||
|
||||
{isLoading ? (
|
||||
|
|
@ -211,6 +196,8 @@ const SearchPageInternal = () => {
|
|||
isBranchFilteringEnabled={isBranchFilteringEnabled}
|
||||
repoUrlTemplates={repoUrlTemplates}
|
||||
repoMetadata={repoMetadata ?? {}}
|
||||
searchDurationMs={searchDurationMs}
|
||||
numMatches={numMatches}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -224,6 +211,8 @@ interface PanelGroupProps {
|
|||
isBranchFilteringEnabled: boolean;
|
||||
repoUrlTemplates: Record<string, string>;
|
||||
repoMetadata: Record<string, Repository>;
|
||||
searchDurationMs: number;
|
||||
numMatches: number;
|
||||
}
|
||||
|
||||
const PanelGroup = ({
|
||||
|
|
@ -233,6 +222,8 @@ const PanelGroup = ({
|
|||
isBranchFilteringEnabled,
|
||||
repoUrlTemplates,
|
||||
repoMetadata,
|
||||
searchDurationMs,
|
||||
numMatches,
|
||||
}: PanelGroupProps) => {
|
||||
const [selectedMatchIndex, setSelectedMatchIndex] = useState(0);
|
||||
const [selectedFile, setSelectedFile] = useState<SearchResultFile | undefined>(undefined);
|
||||
|
|
@ -272,7 +263,7 @@ const PanelGroup = ({
|
|||
/>
|
||||
</ResizablePanel>
|
||||
<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 ~~ */}
|
||||
|
|
@ -281,6 +272,24 @@ const PanelGroup = ({
|
|||
id={'search-results-panel'}
|
||||
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 ? (
|
||||
<SearchResultsPanel
|
||||
fileMatches={filteredFileMatches}
|
||||
|
|
@ -302,7 +311,7 @@ const PanelGroup = ({
|
|||
)}
|
||||
</ResizablePanel>
|
||||
<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 ~~ */}
|
||||
|
|
|
|||
|
|
@ -3,19 +3,18 @@
|
|||
import { NEXT_PUBLIC_DOMAIN_SUB_PATH } from "@/lib/environment.client";
|
||||
import { fileSourceResponseSchema, listRepositoriesResponseSchema, searchResponseSchema } from "@/lib/schemas";
|
||||
import { FileSourceRequest, FileSourceResponse, ListRepositoriesResponse, SearchRequest, SearchResponse } from "@/lib/types";
|
||||
import { measure } from "@/lib/utils";
|
||||
import assert from "assert";
|
||||
|
||||
export const search = async (body: SearchRequest, domain: string): Promise<SearchResponse> => {
|
||||
const path = resolveServerPath("/api/search");
|
||||
const { data: result } = await measure(() => fetch(path, {
|
||||
const result = await fetch(path, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-Org-Domain": domain,
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
}).then(response => response.json()), "client.search");
|
||||
}).then(response => response.json());
|
||||
|
||||
return searchResponseSchema.parse(result);
|
||||
}
|
||||
|
|
|
|||
78
packages/web/src/hooks/useCodeMirrorTheme.ts
Normal file
78
packages/web/src/hooks/useCodeMirrorTheme.ts
Normal 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;
|
||||
}
|
||||
Loading…
Reference in a new issue