mirror of
https://github.com/sourcebot-dev/sourcebot.git
synced 2025-12-12 20:35:24 +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 { 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
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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 &&
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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 : [],
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
))}
|
))}
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 ~~ */}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
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