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 (
- <>
-
-
-
- >
- )
-}
\ 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 (
- <>
-
-
- {
- updateBrowseState({
- isBottomPanelCollapsed: !isBottomPanelCollapsed,
- })
- }}
- >
-
- Explore
-
-
-
+ return (
+ <>
+
+
+ {
+ updateBrowseState({
+ isBottomPanelCollapsed: !isBottomPanelCollapsed,
+ })
+ }}
+ >
+
+ Explore
+
+
+
- {!isBottomPanelCollapsed && (
-
{
- updateBrowseState({ isBottomPanelCollapsed: true })
- }}
- >
-
- Hide
-
- )}
-
-
-
updateBrowseState({ isBottomPanelCollapsed: true })}
- onExpand={() => updateBrowseState({ isBottomPanelCollapsed: false })}
- onResize={(size) => {
- if (!isBottomPanelCollapsed) {
- updateBrowseState({ bottomPanelSize: size });
- }
- }}
- order={order}
- id={"bottom-panel"}
+ {!isBottomPanelCollapsed && (
+ {
+ updateBrowseState({ isBottomPanelCollapsed: true })
+ }}
+ >
+
+ Hide
+
+ )}
+
+
+ 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"}
+ >
+
+
+ {isChatPanelCollapsed && (
+
+
+
+ {
+ panelRef.current?.expand();
+ }}
+ >
+
+
+
+
+
+
+ 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
+
+
+ )
}