diff --git a/packages/web/src/app/[domain]/browse/[...path]/codePreview.tsx b/packages/web/src/app/[domain]/browse/[...path]/codePreview.tsx index 9a7dbdb4..8f6243c7 100644 --- a/packages/web/src/app/[domain]/browse/[...path]/codePreview.tsx +++ b/packages/web/src/app/[domain]/browse/[...path]/codePreview.tsx @@ -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 ( @@ -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 && ( = (previous: T) => T; export type QuickAction = { @@ -119,7 +119,7 @@ const ConfigEditor = (props: ConfigEditorProps, forwardedRef: Ref(props: ConfigEditorProps, forwardedRef: Ref
{ - const info = getRepoCodeHostInfo(repo); return ( @@ -47,7 +46,7 @@ export const FileHeader = ({ {branchDisplayName && (

{/* hack since to make the @ symbol look more centered with the text */} @@ -64,7 +63,9 @@ export const FileHeader = ({

)} · -
+
{!fileNameHighlightRange ? fileName diff --git a/packages/web/src/app/[domain]/search/components/codePreviewPanel/codePreview.tsx b/packages/web/src/app/[domain]/search/components/codePreviewPanel/codePreview.tsx index 983b3706..698fcd36 100644 --- a/packages/web/src/app/[domain]/search/components/codePreviewPanel/codePreview.tsx +++ b/packages/web/src/app/[domain]/search/components/codePreviewPanel/codePreview.tsx @@ -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(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 (
-
+
{/* Gutter icon */}
@@ -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 && diff --git a/packages/web/src/app/[domain]/search/components/codePreviewPanel/index.tsx b/packages/web/src/app/[domain]/search/components/codePreviewPanel/index.tsx index 10ca1111..340bc0af 100644 --- a/packages/web/src/app/[domain]/search/components/codePreviewPanel/index.tsx +++ b/packages/web/src/app/[domain]/search/components/codePreviewPanel/index.tsx @@ -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 => { if (!fileMatch) { @@ -88,6 +88,13 @@ export const CodePreviewPanel = ({ enabled: fileMatch !== undefined, }); + if (isLoading) { + return
+ +

Loading...

+
+ } + return ( {displayName}

-
+
{countText}
diff --git a/packages/web/src/app/[domain]/search/components/searchResultsPanel/codePreview.tsx b/packages/web/src/app/[domain]/search/components/searchResultsPanel/codePreview.tsx index 0491a642..d87cde69 100644 --- a/packages/web/src/app/[domain]/search/components/searchResultsPanel/codePreview.tsx +++ b/packages/web/src/app/[domain]/search/components/searchResultsPanel/codePreview.tsx @@ -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(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 : [], diff --git a/packages/web/src/app/[domain]/search/components/searchResultsPanel/fileMatchContainer.tsx b/packages/web/src/app/[domain]/search/components/searchResultsPanel/fileMatchContainer.tsx index 539c19b8..0a8e74a6 100644 --- a/packages/web/src/app/[domain]/search/components/searchResultsPanel/fileMatchContainer.tsx +++ b/packages/web/src/app/[domain]/search/components/searchResultsPanel/fileMatchContainer.tsx @@ -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; + 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 = ({
{/* Title */}
{ onOpenFile(); }} @@ -119,7 +124,7 @@ export const FileMatchContainer = ({ }} /> {(index !== matches.length - 1 || isMoreContentButtonVisible) && ( - + )}
))} diff --git a/packages/web/src/app/[domain]/search/components/searchResultsPanel/index.tsx b/packages/web/src/app/[domain]/search/components/searchResultsPanel/index.tsx index 67ff7e1e..5eb5ea10 100644 --- a/packages/web/src/app/[domain]/search/components/searchResultsPanel/index.tsx +++ b/packages/web/src/app/[domain]/search/components/searchResultsPanel/index.tsx @@ -124,36 +124,40 @@ export const SearchResultsPanel = ({ position: "relative", }} > - {virtualizer.getVirtualItems().map((virtualRow) => ( -
- { - onOpenFileMatch(fileMatches[virtualRow.index]); + {virtualizer.getVirtualItems().map((virtualRow) => { + const file = fileMatches[virtualRow.index]; + return ( +
{ - onMatchIndexChanged(matchIndex); - }} - showAllMatches={showAllMatchesStates[virtualRow.index]} - onShowAllMatchesButtonClicked={() => { - onShowAllMatchesButtonClicked(virtualRow.index); - }} - isBranchFilteringEnabled={isBranchFilteringEnabled} - repoMetadata={repoMetadata} - /> -
- ))} + > + { + onOpenFileMatch(file); + }} + onMatchIndexChanged={(matchIndex) => { + onMatchIndexChanged(matchIndex); + }} + showAllMatches={showAllMatchesStates[virtualRow.index]} + onShowAllMatchesButtonClicked={() => { + onShowAllMatchesButtonClicked(virtualRow.index); + }} + isBranchFilteringEnabled={isBranchFilteringEnabled} + repoMetadata={repoMetadata} + yOffset={virtualRow.start} + /> +
+ ) + })}
{isLoadMoreButtonVisible && (
diff --git a/packages/web/src/app/[domain]/search/components/searchResultsPanel/lightweightCodeMirror.tsx b/packages/web/src/app/[domain]/search/components/searchResultsPanel/lightweightCodeMirror.tsx index 5cc774b4..f6d3227e 100644 --- a/packages/web/src/app/[domain]/search/components/searchResultsPanel/lightweightCodeMirror.tsx +++ b/packages/web/src/app/[domain]/search/components/searchResultsPanel/lightweightCodeMirror.tsx @@ -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(({ className, }, ref) => { const editor = useRef(null); - const [view, setView] = useState(); - const [state, setState] = useState(); + const viewRef = useRef(); + const stateRef = useRef(); 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(({ 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 (
{ 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 (
@@ -176,26 +181,6 @@ const SearchPageInternal = () => { domain={domain} /> - {!isLoading && ( -
- { - fileMatches.length > 0 ? ( -

{`[${searchDurationMs} ms] Found ${numMatches} matches in ${fileMatches.length} ${fileMatches.length > 1 ? 'files' : 'file'}`}

- ) : ( -

No results

- ) - } - {isMoreResultsButtonVisible && ( -
- (load more) -
- )} -
- )} -
{isLoading ? ( @@ -211,6 +196,8 @@ const SearchPageInternal = () => { isBranchFilteringEnabled={isBranchFilteringEnabled} repoUrlTemplates={repoUrlTemplates} repoMetadata={repoMetadata ?? {}} + searchDurationMs={searchDurationMs} + numMatches={numMatches} /> )}
@@ -224,6 +211,8 @@ interface PanelGroupProps { isBranchFilteringEnabled: boolean; repoUrlTemplates: Record; repoMetadata: Record; + 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(undefined); @@ -272,7 +263,7 @@ const PanelGroup = ({ /> {/* ~~ Search results ~~ */} @@ -281,6 +272,24 @@ const PanelGroup = ({ id={'search-results-panel'} order={2} > +
+ + { + fileMatches.length > 0 ? ( +

{`[${searchDurationMs} ms] Found ${numMatches} matches in ${fileMatches.length} ${fileMatches.length > 1 ? 'files' : 'file'}`}

+ ) : ( +

No results

+ ) + } + {isMoreResultsButtonVisible && ( +
+ (load more) +
+ )} +
{filteredFileMatches.length > 0 ? ( {/* ~~ Code preview ~~ */} diff --git a/packages/web/src/app/api/(client)/client.ts b/packages/web/src/app/api/(client)/client.ts index 0a98a0b6..ac83d370 100644 --- a/packages/web/src/app/api/(client)/client.ts +++ b/packages/web/src/app/api/(client)/client.ts @@ -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 => { 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); } diff --git a/packages/web/src/hooks/useCodeMirrorTheme.ts b/packages/web/src/hooks/useCodeMirrorTheme.ts new file mode 100644 index 00000000..7370483e --- /dev/null +++ b/packages/web/src/hooks/useCodeMirrorTheme.ts @@ -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; +} \ No newline at end of file