diff --git a/packages/web/src/app/[domain]/browse/[...path]/components/codePreviewPanel.tsx b/packages/web/src/app/[domain]/browse/[...path]/components/codePreviewPanel.tsx index 01a84447e..0f4b0f545 100644 --- a/packages/web/src/app/[domain]/browse/[...path]/components/codePreviewPanel.tsx +++ b/packages/web/src/app/[domain]/browse/[...path]/components/codePreviewPanel.tsx @@ -7,76 +7,81 @@ import Image from "next/image"; import { PureCodePreviewPanel } from "./pureCodePreviewPanel"; interface CodePreviewPanelProps { - path: string; - repoName: string; - revisionName?: string; + path: string; + repoName: string; + revisionName?: string; } export const CodePreviewPanel = async ({ path, repoName, revisionName }: CodePreviewPanelProps) => { - const [fileSourceResponse, repoInfoResponse] = await Promise.all([ - getFileSource({ - fileName: path, - repository: repoName, - branch: revisionName, - }), - getRepoInfoByName(repoName), - ]); + const [fileSourceResponse, repoInfoResponse] = await Promise.all([ + getFileSource({ + fileName: path, + repository: repoName, + branch: revisionName, + }), + getRepoInfoByName(repoName), + ]); - if (isServiceError(fileSourceResponse) || isServiceError(repoInfoResponse)) { - return
Error loading file source
- } + if (isServiceError(fileSourceResponse) || isServiceError(repoInfoResponse)) { + return
Error loading file source
+ } - const codeHostInfo = getCodeHostInfoForRepo({ - codeHostType: repoInfoResponse.codeHostType, - name: repoInfoResponse.name, - displayName: repoInfoResponse.displayName, - webUrl: repoInfoResponse.webUrl, - }); + const codeHostInfo = getCodeHostInfoForRepo({ + codeHostType: repoInfoResponse.codeHostType, + name: repoInfoResponse.name, + displayName: repoInfoResponse.displayName, + webUrl: repoInfoResponse.webUrl, + }); - // @todo: this is a hack to support linking to files for ADO. ADO doesn't support web urls with HEAD so we replace it with main. THis - // will break if the default branch is not main. - const fileWebUrl = repoInfoResponse.codeHostType === "azuredevops" && fileSourceResponse.webUrl ? - fileSourceResponse.webUrl.replace("version=GBHEAD", "version=GBmain") : fileSourceResponse.webUrl; + // @todo: this is a hack to support linking to files for ADO. ADO doesn't support web urls with HEAD so we replace it with main. THis + // will break if the default branch is not main. + const fileWebUrl = repoInfoResponse.codeHostType === "azuredevops" && fileSourceResponse.webUrl ? + fileSourceResponse.webUrl.replace("version=GBHEAD", "version=GBmain") : fileSourceResponse.webUrl; - return ( - <> -
- + return ( + <> +
+
+ +
- {(fileWebUrl && codeHostInfo) && ( +
+ {(fileWebUrl && codeHostInfo) && ( + + {codeHostInfo.codeHostName} + Open in {codeHostInfo.codeHostName} + + )} +
+
- - {codeHostInfo.codeHostName} - Open in {codeHostInfo.codeHostName} - - )} -
- - - - ) -} \ No newline at end of file + + + + + ) +} diff --git a/packages/web/src/app/[domain]/browse/[...path]/page.tsx b/packages/web/src/app/[domain]/browse/[...path]/page.tsx index 2cadab600..1440a73da 100644 --- a/packages/web/src/app/[domain]/browse/[...path]/page.tsx +++ b/packages/web/src/app/[domain]/browse/[...path]/page.tsx @@ -66,44 +66,44 @@ export async function generateMetadata({ params: paramsPromise }: Props): Promis } interface BrowsePageProps { - params: Promise<{ - path: string[]; - }>; + params: Promise<{ + path: string[]; + }>; } export default async function BrowsePage(props: BrowsePageProps) { - const params = await props.params; + const params = await props.params; - const { - path: _rawPath, - } = params; + const { + path: _rawPath, + } = params; - const rawPath = _rawPath.join('/'); - const { repoName, revisionName, path, pathType } = getBrowseParamsFromPathParam(rawPath); + const rawPath = _rawPath.join('/'); + const { repoName, revisionName, path, pathType } = getBrowseParamsFromPathParam(rawPath); - return ( -
- - - Loading... -
- }> - {pathType === 'blob' ? ( - - ) : ( - - )} - + return ( +
+ + + Loading...
- ) + }> + {pathType === 'blob' ? ( + + ) : ( + + )} + + + ) } diff --git a/packages/web/src/app/[domain]/browse/browseStateProvider.tsx b/packages/web/src/app/[domain]/browse/browseStateProvider.tsx index a3dea45b4..b7275c952 100644 --- a/packages/web/src/app/[domain]/browse/browseStateProvider.tsx +++ b/packages/web/src/app/[domain]/browse/browseStateProvider.tsx @@ -4,78 +4,82 @@ import { useNonEmptyQueryParam } from "@/hooks/useNonEmptyQueryParam"; import { createContext, useCallback, useEffect, useState } from "react"; export interface BrowseState { - selectedSymbolInfo?: { - symbolName: string; - repoName: string; - revisionName: string; - language: string; - } - isBottomPanelCollapsed: boolean; - isFileTreePanelCollapsed: boolean; - isFileSearchOpen: boolean; - activeExploreMenuTab: "references" | "definitions"; - bottomPanelSize: number; + selectedSymbolInfo?: { + symbolName: string; + repoName: string; + revisionName: string; + language: string; + } + isBottomPanelCollapsed: boolean; + isChatPanelCollapsed: boolean; + isFileTreePanelCollapsed: boolean; + isFileSearchOpen: boolean; + activeExploreMenuTab: "references" | "definitions"; + bottomPanelSize: number; + chatPanelSize: number; } const defaultState: BrowseState = { - selectedSymbolInfo: undefined, - isBottomPanelCollapsed: true, - isFileTreePanelCollapsed: false, - isFileSearchOpen: false, - activeExploreMenuTab: "references", - bottomPanelSize: 35, + selectedSymbolInfo: undefined, + isBottomPanelCollapsed: true, + isFileTreePanelCollapsed: false, + isFileSearchOpen: false, + activeExploreMenuTab: "references", + bottomPanelSize: 35, + isChatPanelCollapsed: true, + chatPanelSize: 20, }; export const SET_BROWSE_STATE_QUERY_PARAM = "setBrowseState"; export const BrowseStateContext = createContext<{ - state: BrowseState; - updateBrowseState: (state: Partial) => void; + state: BrowseState; + updateBrowseState: (state: Partial) => void; }>({ - state: defaultState, - updateBrowseState: () => {}, + state: defaultState, + updateBrowseState: () => { }, }); interface BrowseStateProviderProps { - children: React.ReactNode; + children: React.ReactNode; } export const BrowseStateProvider = ({ children }: BrowseStateProviderProps) => { - const [state, setState] = useState(defaultState); + const [state, setState] = useState(defaultState); - const hydratedBrowseState = useNonEmptyQueryParam(SET_BROWSE_STATE_QUERY_PARAM); + const hydratedBrowseState = useNonEmptyQueryParam(SET_BROWSE_STATE_QUERY_PARAM); - const onUpdateState = useCallback((state: Partial) => { - setState((prevState) => ({ - ...prevState, - ...state, - })); - }, []); + const onUpdateState = useCallback((state: Partial) => { + setState((prevState) => ({ + ...prevState, + ...state, + })); + }, []); - useEffect(() => { - if (hydratedBrowseState) { - try { - const parsedState = JSON.parse(hydratedBrowseState) as Partial; - onUpdateState(parsedState); - } catch (error) { - console.error("Error parsing hydratedBrowseState", error); - } + useEffect(() => { + if (hydratedBrowseState) { + try { + const parsedState = JSON.parse(hydratedBrowseState) as Partial; + onUpdateState(parsedState); + } catch (error) { + console.error("Error parsing hydratedBrowseState", error); + } - // Remove the query param - const url = new URL(window.location.href); - url.searchParams.delete(SET_BROWSE_STATE_QUERY_PARAM); - window.history.replaceState({}, '', url.toString()); - } - }, [hydratedBrowseState, onUpdateState]); + // Remove the query param + const url = new URL(window.location.href); + url.searchParams.delete(SET_BROWSE_STATE_QUERY_PARAM); + window.history.replaceState({}, '', url.toString()); + } + }, [hydratedBrowseState, onUpdateState]); - return ( - - {children} - - ); -}; \ No newline at end of file + return ( + + {children} + + ); +}; diff --git a/packages/web/src/app/[domain]/browse/components/bottomPanel.tsx b/packages/web/src/app/[domain]/browse/components/bottomPanel.tsx index 4a155207a..18a4028f7 100644 --- a/packages/web/src/app/[domain]/browse/components/bottomPanel.tsx +++ b/packages/web/src/app/[domain]/browse/components/bottomPanel.tsx @@ -21,120 +21,120 @@ export const BOTTOM_PANEL_MAX_SIZE = 65; const CODE_NAV_DOCS_URL = "https://docs.sourcebot.dev/docs/features/code-navigation"; interface BottomPanelProps { - order: number; + order: number; } export const BottomPanel = ({ order }: BottomPanelProps) => { - const panelRef = useRef(null); - const hasCodeNavEntitlement = useHasEntitlement("code-nav"); - const domain = useDomain(); - const router = useRouter(); + const panelRef = useRef(null); + const hasCodeNavEntitlement = useHasEntitlement("code-nav"); + const domain = useDomain(); + const router = useRouter(); - const { - state: { selectedSymbolInfo, isBottomPanelCollapsed, bottomPanelSize }, - updateBrowseState, - } = useBrowseState(); + const { + state: { selectedSymbolInfo, isBottomPanelCollapsed, bottomPanelSize }, + updateBrowseState, + } = useBrowseState(); - useEffect(() => { - if (isBottomPanelCollapsed) { - panelRef.current?.collapse(); - } else { - panelRef.current?.expand(); - } - }, [isBottomPanelCollapsed]); + useEffect(() => { + if (isBottomPanelCollapsed) { + panelRef.current?.collapse(); + } else { + panelRef.current?.expand(); + } + }, [isBottomPanelCollapsed]); - useHotkeys("shift+mod+e", (event) => { - event.preventDefault(); - updateBrowseState({ isBottomPanelCollapsed: !isBottomPanelCollapsed }); - }, { - enableOnFormTags: true, - enableOnContentEditable: true, - description: "Open Explore Panel", - }); + useHotkeys("shift+mod+e", (event) => { + event.preventDefault(); + updateBrowseState({ isBottomPanelCollapsed: !isBottomPanelCollapsed }); + }, { + enableOnFormTags: true, + enableOnContentEditable: true, + description: "Open Explore Panel", + }); - return ( - <> -
-
- -
+ return ( + <> +
+
+ +
- {!isBottomPanelCollapsed && ( - - )} -
- - updateBrowseState({ isBottomPanelCollapsed: true })} - onExpand={() => updateBrowseState({ isBottomPanelCollapsed: false })} - onResize={(size) => { - if (!isBottomPanelCollapsed) { - updateBrowseState({ bottomPanelSize: size }); - } - }} - order={order} - id={"bottom-panel"} + {!isBottomPanelCollapsed && ( + + )} +
+ + updateBrowseState({ isBottomPanelCollapsed: true })} + onExpand={() => updateBrowseState({ isBottomPanelCollapsed: false })} + onResize={(size) => { + if (!isBottomPanelCollapsed) { + updateBrowseState({ bottomPanelSize: size }); + } + }} + order={order} + id={"bottom-panel"} + > + {!hasCodeNavEntitlement ? ( +
+ +

+ Code navigation is not enabled for router.push(`/${domain}/settings/license`)}>your plan. +

+ + + Learn more + +
+ ) : !selectedSymbolInfo ? ( +
+ +

No symbol selected

+ - {!hasCodeNavEntitlement ? ( -
- -

- Code navigation is not enabled for router.push(`/${domain}/settings/license`)}>your plan. -

- - - Learn more - -
- ) : !selectedSymbolInfo ? ( -
- -

No symbol selected

- - Learn more - -
- ) : ( - - )} - - - ) + Learn more + +
+ ) : ( + + )} +
+ + ) } diff --git a/packages/web/src/app/[domain]/browse/components/chatPanel.tsx b/packages/web/src/app/[domain]/browse/components/chatPanel.tsx new file mode 100644 index 000000000..dc34d3d01 --- /dev/null +++ b/packages/web/src/app/[domain]/browse/components/chatPanel.tsx @@ -0,0 +1,94 @@ +'use client' + +import { KeyboardShortcutHint } from "@/app/components/keyboardShortcutHint" +import { Button } from "@/components/ui/button" +import { RiRobot3Line } from "react-icons/ri"; +import { useBrowseState } from "../hooks/useBrowseState"; +import { ImperativePanelHandle } from "react-resizable-panels"; +import { useEffect, useRef } from "react"; +import { useHotkeys } from "react-hotkeys-hook"; +import { Separator } from "@/components/ui/separator"; +import { ResizablePanel } from "@/components/ui/resizable"; +import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; + +export const CHAT_PANEL_MIN_SIZE = 5; +export const CHAT_PANEL_MAX_SIZE = 50; + +interface ChatPanelProps { + order: number; +} + +export const ChatPanel = ({ order }: ChatPanelProps) => { + const panelRef = useRef(null); + const { + state: { isChatPanelCollapsed, chatPanelSize }, + updateBrowseState + } = useBrowseState(); + + useEffect(() => { + if (isChatPanelCollapsed) { + panelRef.current?.collapse(); + } else { + panelRef.current?.expand(); + } + }, [isChatPanelCollapsed]); + + useHotkeys("shift+mod+o", (event) => { + event.preventDefault(); + updateBrowseState({ isChatPanelCollapsed: !isChatPanelCollapsed }); + }, { + enableOnFormTags: true, + enableOnContentEditable: true, + description: "Open Chat Panel" + }); + + return ( + <> + updateBrowseState({ isChatPanelCollapsed: true })} + onExpand={() => updateBrowseState({ isChatPanelCollapsed: false })} + onResize={(size) => { + if (!isChatPanelCollapsed) { + updateBrowseState({ chatPanelSize: size }); + } + }} + order={order} + id={"chat-panel"} + > +
+

Chat goes here

+
+
+ {isChatPanelCollapsed && ( +
+ + + + + + + + Open AI Chat + + +
+ )} + + ) +} diff --git a/packages/web/src/app/[domain]/browse/layout.tsx b/packages/web/src/app/[domain]/browse/layout.tsx index 6807a38fa..d556206ba 100644 --- a/packages/web/src/app/[domain]/browse/layout.tsx +++ b/packages/web/src/app/[domain]/browse/layout.tsx @@ -2,6 +2,7 @@ import { ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable"; import { BottomPanel } from "./components/bottomPanel"; +import { ChatPanel } from "./components/chatPanel"; import { AnimatedResizableHandle } from "@/components/ui/animatedResizableHandle"; import { BrowseStateProvider } from "./browseStateProvider"; import { FileTreePanel } from "@/features/fileTree/components/fileTreePanel"; @@ -12,58 +13,62 @@ import { useDomain } from "@/hooks/useDomain"; import { SearchBar } from "../components/searchBar"; interface LayoutProps { - children: React.ReactNode; + children: React.ReactNode; } export default function Layout({ - children, + children, }: LayoutProps) { - const { repoName, revisionName } = useBrowseParams(); - const domain = useDomain(); + const { repoName, revisionName } = useBrowseParams(); + const domain = useDomain(); - return ( - -
- - - - - + return ( + +
+ + + + + - + - - - - {children} - - - - - - -
- -
- ); + + + + {children} + + + + + + + + + +
+
+ +
+ ); } diff --git a/packages/web/src/app/[domain]/components/searchBar/searchBar.tsx b/packages/web/src/app/[domain]/components/searchBar/searchBar.tsx index 6dbd47cd8..d136c8f6e 100644 --- a/packages/web/src/app/[domain]/components/searchBar/searchBar.tsx +++ b/packages/web/src/app/[domain]/components/searchBar/searchBar.tsx @@ -4,27 +4,27 @@ import { useClickListener } from "@/hooks/useClickListener"; import { SearchQueryParams } from "@/lib/types"; import { cn, createPathWithQueryParams } from "@/lib/utils"; import { - cursorCharLeft, - cursorCharRight, - cursorDocEnd, - cursorDocStart, - cursorLineBoundaryBackward, - cursorLineBoundaryForward, - deleteCharBackward, - deleteCharForward, - deleteGroupBackward, - deleteGroupForward, - deleteLineBoundaryBackward, - deleteLineBoundaryForward, - history, - historyKeymap, - selectAll, - selectCharLeft, - selectCharRight, - selectDocEnd, - selectDocStart, - selectLineBoundaryBackward, - selectLineBoundaryForward + cursorCharLeft, + cursorCharRight, + cursorDocEnd, + cursorDocStart, + cursorLineBoundaryBackward, + cursorLineBoundaryForward, + deleteCharBackward, + deleteCharForward, + deleteGroupBackward, + deleteGroupForward, + deleteLineBoundaryBackward, + deleteLineBoundaryForward, + history, + historyKeymap, + selectAll, + selectCharLeft, + selectCharRight, + selectDocEnd, + selectDocStart, + selectLineBoundaryBackward, + selectLineBoundaryForward } from "@codemirror/commands"; import { tags as t } from '@lezer/highlight'; import { createTheme } from '@uiw/codemirror-themes'; @@ -47,321 +47,321 @@ import { createAuditAction } from "@/ee/features/audit/actions"; import tailwind from "@/tailwind"; interface SearchBarProps { - className?: string; - size?: "default" | "sm"; - defaultQuery?: string; - autoFocus?: boolean; + className?: string; + size?: "default" | "sm"; + defaultQuery?: string; + autoFocus?: boolean; } const searchBarKeymap: readonly KeyBinding[] = ([ - { key: "ArrowLeft", run: cursorCharLeft, shift: selectCharLeft, preventDefault: true }, - { key: "ArrowRight", run: cursorCharRight, shift: selectCharRight, preventDefault: true }, + { key: "ArrowLeft", run: cursorCharLeft, shift: selectCharLeft, preventDefault: true }, + { key: "ArrowRight", run: cursorCharRight, shift: selectCharRight, preventDefault: true }, - { key: "Home", run: cursorLineBoundaryBackward, shift: selectLineBoundaryBackward, preventDefault: true }, - { key: "Mod-Home", run: cursorDocStart, shift: selectDocStart }, + { key: "Home", run: cursorLineBoundaryBackward, shift: selectLineBoundaryBackward, preventDefault: true }, + { key: "Mod-Home", run: cursorDocStart, shift: selectDocStart }, - { key: "End", run: cursorLineBoundaryForward, shift: selectLineBoundaryForward, preventDefault: true }, - { key: "Mod-End", run: cursorDocEnd, shift: selectDocEnd }, + { key: "End", run: cursorLineBoundaryForward, shift: selectLineBoundaryForward, preventDefault: true }, + { key: "Mod-End", run: cursorDocEnd, shift: selectDocEnd }, - { key: "Mod-a", run: selectAll }, + { key: "Mod-a", run: selectAll }, - { key: "Backspace", run: deleteCharBackward, shift: deleteCharBackward }, - { key: "Delete", run: deleteCharForward }, - { key: "Mod-Backspace", mac: "Alt-Backspace", run: deleteGroupBackward }, - { key: "Mod-Delete", mac: "Alt-Delete", run: deleteGroupForward }, - { mac: "Mod-Backspace", run: deleteLineBoundaryBackward }, - { mac: "Mod-Delete", run: deleteLineBoundaryForward } + { key: "Backspace", run: deleteCharBackward, shift: deleteCharBackward }, + { key: "Delete", run: deleteCharForward }, + { key: "Mod-Backspace", mac: "Alt-Backspace", run: deleteGroupBackward }, + { key: "Mod-Delete", mac: "Alt-Delete", run: deleteGroupForward }, + { mac: "Mod-Backspace", run: deleteLineBoundaryBackward }, + { mac: "Mod-Delete", run: deleteLineBoundaryForward } ] as KeyBinding[]).concat(historyKeymap); const searchBarContainerVariants = cva( - "search-bar-container flex items-center justify-center py-0.5 px-2 border rounded-md relative", - { - variants: { - size: { - default: "min-h-10", - sm: "min-h-8" - } - }, - defaultVariants: { - size: "default", - } + "search-bar-container flex items-center justify-center py-0.5 px-2 border rounded-md relative", + { + variants: { + size: { + default: "min-h-10", + sm: "min-h-8" + } + }, + defaultVariants: { + size: "default", } + } ); export const SearchBar = ({ - className, - size, - defaultQuery, - autoFocus, + className, + size, + defaultQuery, + autoFocus, }: SearchBarProps) => { - const router = useRouter(); - const domain = useDomain(); - const suggestionBoxRef = useRef(null); - const editorRef = useRef(null); - const [cursorPosition, setCursorPosition] = useState(0); - const [isSuggestionsEnabled, setIsSuggestionsEnabled] = useState(false); - const [isSuggestionsBoxFocused, setIsSuggestionsBoxFocused] = useState(false); - const [isHistorySearchEnabled, setIsHistorySearchEnabled] = useState(false); - - const focusEditor = useCallback(() => editorRef.current?.view?.focus(), []); - const focusSuggestionsBox = useCallback(() => suggestionBoxRef.current?.focus(), []); + const router = useRouter(); + const domain = useDomain(); + const suggestionBoxRef = useRef(null); + const editorRef = useRef(null); + const [cursorPosition, setCursorPosition] = useState(0); + const [isSuggestionsEnabled, setIsSuggestionsEnabled] = useState(false); + const [isSuggestionsBoxFocused, setIsSuggestionsBoxFocused] = useState(false); + const [isHistorySearchEnabled, setIsHistorySearchEnabled] = useState(false); - const [_query, setQuery] = useState(defaultQuery ?? ""); - const query = useMemo(() => { - // Replace any newlines with spaces to handle - // copy & pasting text with newlines. - return _query.replaceAll(/\n/g, " "); - }, [_query]); + const focusEditor = useCallback(() => editorRef.current?.view?.focus(), []); + const focusSuggestionsBox = useCallback(() => suggestionBoxRef.current?.focus(), []); - // When the user navigates backwards/forwards while on the - // search page (causing the `query` search param to change), - // we want to update what query is displayed in the search bar. - useEffect(() => { - if (defaultQuery) { - setQuery(defaultQuery); - } - }, [defaultQuery]) - - const { suggestionMode, suggestionQuery } = useSuggestionModeAndQuery({ - isSuggestionsEnabled, - isHistorySearchEnabled, - cursorPosition, - query, - }); + const [_query, setQuery] = useState(defaultQuery ?? ""); + const query = useMemo(() => { + // Replace any newlines with spaces to handle + // copy & pasting text with newlines. + return _query.replaceAll(/\n/g, " "); + }, [_query]); - const suggestionData = useSuggestionsData({ - suggestionMode, - suggestionQuery, - }); + // When the user navigates backwards/forwards while on the + // search page (causing the `query` search param to change), + // we want to update what query is displayed in the search bar. + useEffect(() => { + if (defaultQuery) { + setQuery(defaultQuery); + } + }, [defaultQuery]) - const theme = useMemo(() => { - return createTheme({ - theme: 'light', - settings: { - background: tailwind.theme.colors.background, - foreground: tailwind.theme.colors.foreground, - caret: '#AEAFAD', - }, - styles: [ - { - tag: t.keyword, - color: tailwind.theme.colors.highlight, - }, - { - tag: t.string, - color: '#2aa198', - }, - { - tag: t.operator, - color: '#d33682', - }, - { - tag: t.paren, - color: tailwind.theme.colors.highlight, - }, - ], - }); - }, []); + const { suggestionMode, suggestionQuery } = useSuggestionModeAndQuery({ + isSuggestionsEnabled, + isHistorySearchEnabled, + cursorPosition, + query, + }); - const extensions = useMemo(() => { - return [ - keymap.of(searchBarKeymap), - history(), - zoekt(), - EditorView.lineWrapping, - EditorView.updateListener.of(update => { - if (update.selectionSet) { - const selection = update.state.selection.main; - if (selection.empty) { - setCursorPosition(selection.anchor); - } - } - }) - ]; - }, []); + const suggestionData = useSuggestionsData({ + suggestionMode, + suggestionQuery, + }); - // Hotkey to focus the search bar. - useHotkeys('/', (event) => { - event.preventDefault(); - focusEditor(); - setIsSuggestionsEnabled(true); - if (editorRef.current?.view) { - cursorDocEnd({ - state: editorRef.current.view.state, - dispatch: editorRef.current.view.dispatch, - }); - } + const theme = useMemo(() => { + return createTheme({ + theme: 'light', + settings: { + background: tailwind.theme.colors.background, + foreground: tailwind.theme.colors.foreground, + caret: '#AEAFAD', + }, + styles: [ + { + tag: t.keyword, + color: tailwind.theme.colors.highlight, + }, + { + tag: t.string, + color: '#2aa198', + }, + { + tag: t.operator, + color: '#d33682', + }, + { + tag: t.paren, + color: tailwind.theme.colors.highlight, + }, + ], }); + }, []); - // Collapse the suggestions box if the user clicks outside of the search bar container. - useClickListener('.search-bar-container', (isElementClicked) => { - if (!isElementClicked) { - setIsSuggestionsEnabled(false); - } else { - setIsSuggestionsEnabled(true); + const extensions = useMemo(() => { + return [ + keymap.of(searchBarKeymap), + history(), + zoekt(), + EditorView.lineWrapping, + EditorView.updateListener.of(update => { + if (update.selectionSet) { + const selection = update.state.selection.main; + if (selection.empty) { + setCursorPosition(selection.anchor); + } } - }); + }) + ]; + }, []); + + // Hotkey to focus the search bar. + useHotkeys('/', (event) => { + event.preventDefault(); + focusEditor(); + setIsSuggestionsEnabled(true); + if (editorRef.current?.view) { + cursorDocEnd({ + state: editorRef.current.view.state, + dispatch: editorRef.current.view.dispatch, + }); + } + }); + + // Collapse the suggestions box if the user clicks outside of the search bar container. + useClickListener('.search-bar-container', (isElementClicked) => { + if (!isElementClicked) { + setIsSuggestionsEnabled(false); + } else { + setIsSuggestionsEnabled(true); + } + }); - const onSubmit = useCallback((query: string) => { - setIsSuggestionsEnabled(false); - setIsHistorySearchEnabled(false); + const onSubmit = useCallback((query: string) => { + setIsSuggestionsEnabled(false); + setIsHistorySearchEnabled(false); - createAuditAction({ - action: "user.performed_code_search", - metadata: { - message: query, - }, - }, domain) + createAuditAction({ + action: "user.performed_code_search", + metadata: { + message: query, + }, + }, domain) - const url = createPathWithQueryParams(`/${domain}/search`, - [SearchQueryParams.query, query], - ); - router.push(url); - }, [domain, router]); + const url = createPathWithQueryParams(`/${domain}/search`, + [SearchQueryParams.query, query], + ); + router.push(url); + }, [domain, router]); - return ( -
{ - if (e.key === 'Enter') { - e.preventDefault(); - setIsSuggestionsEnabled(false); - onSubmit(query); - } + return ( +
{ + if (e.key === 'Enter') { + e.preventDefault(); + setIsSuggestionsEnabled(false); + onSubmit(query); + } - if (e.key === 'Escape') { - e.preventDefault(); - setIsSuggestionsEnabled(false); - } + if (e.key === 'Escape') { + e.preventDefault(); + setIsSuggestionsEnabled(false); + } - if (e.key === 'ArrowDown') { - e.preventDefault(); - setIsSuggestionsEnabled(true); - focusSuggestionsBox(); - } + if (e.key === 'ArrowDown') { + e.preventDefault(); + setIsSuggestionsEnabled(true); + focusSuggestionsBox(); + } - if (e.key === 'ArrowUp') { - e.preventDefault(); - } - }} - > - { - setQuery(""); - setIsHistorySearchEnabled(!isHistorySearchEnabled); - setIsSuggestionsEnabled(true); - focusEditor(); - }} - /> - - { - setQuery(value); - // Whenever the user types, we want to re-enable - // the suggestions box. - setIsSuggestionsEnabled(true); - }} - theme={theme} - basicSetup={false} - extensions={extensions} - indentWithTab={false} - autoFocus={autoFocus ?? false} - /> - - -
- -
-
- - Focus search bar - -
- { - setQuery(newQuery); + if (e.key === 'ArrowUp') { + e.preventDefault(); + } + }} + > + { + setQuery(""); + setIsHistorySearchEnabled(!isHistorySearchEnabled); + setIsSuggestionsEnabled(true); + focusEditor(); + }} + /> + + { + setQuery(value); + // Whenever the user types, we want to re-enable + // the suggestions box. + setIsSuggestionsEnabled(true); + }} + theme={theme} + basicSetup={false} + extensions={extensions} + indentWithTab={false} + autoFocus={autoFocus ?? false} + /> + + +
+ +
+
+ + Focus search bar + +
+ { + setQuery(newQuery); - // Move the cursor to it's new position. - // @note : normally, react-codemirror handles syncing `query` - // and the document state, but this happens on re-render. Since - // we want to move the cursor before the component re-renders, - // we manually update the document state inline. - editorRef.current?.view?.dispatch({ - changes: { from: 0, to: query.length, insert: newQuery }, - annotations: [Annotation.define().of(true)], - }); + // Move the cursor to it's new position. + // @note : normally, react-codemirror handles syncing `query` + // and the document state, but this happens on re-render. Since + // we want to move the cursor before the component re-renders, + // we manually update the document state inline. + editorRef.current?.view?.dispatch({ + changes: { from: 0, to: query.length, insert: newQuery }, + annotations: [Annotation.define().of(true)], + }); - editorRef.current?.view?.dispatch({ - selection: { anchor: newCursorPosition, head: newCursorPosition }, - }); + editorRef.current?.view?.dispatch({ + selection: { anchor: newCursorPosition, head: newCursorPosition }, + }); - // Re-focus the editor since suggestions cause focus to be lost (both click & keyboard) - editorRef.current?.view?.focus(); + // Re-focus the editor since suggestions cause focus to be lost (both click & keyboard) + editorRef.current?.view?.focus(); - if (autoSubmit) { - onSubmit(newQuery); - } - }} - isEnabled={isSuggestionsEnabled} - onReturnFocus={() => { - focusEditor(); - }} - isFocused={isSuggestionsBoxFocused} - onFocus={() => { - setIsSuggestionsBoxFocused(document.activeElement === suggestionBoxRef.current); - }} - onBlur={() => { - setIsSuggestionsBoxFocused(document.activeElement === suggestionBoxRef.current); - }} - cursorPosition={cursorPosition} - {...suggestionData} - /> -
- ) + if (autoSubmit) { + onSubmit(newQuery); + } + }} + isEnabled={isSuggestionsEnabled} + onReturnFocus={() => { + focusEditor(); + }} + isFocused={isSuggestionsBoxFocused} + onFocus={() => { + setIsSuggestionsBoxFocused(document.activeElement === suggestionBoxRef.current); + }} + onBlur={() => { + setIsSuggestionsBoxFocused(document.activeElement === suggestionBoxRef.current); + }} + cursorPosition={cursorPosition} + {...suggestionData} + /> +
+ ) } const SearchHistoryButton = ({ - isToggled, - onClick, + isToggled, + onClick, }: { - isToggled: boolean, - onClick: () => void + isToggled: boolean, + onClick: () => void }) => { - return ( - - - {/* @see : https://github.com/shadcn-ui/ui/issues/1988#issuecomment-1980597269 */} -
- - - -
-
- - Search history - -
- ) + return ( + + + {/* @see : https://github.com/shadcn-ui/ui/issues/1988#issuecomment-1980597269 */} +
+ + + +
+
+ + Search history + +
+ ) }