From c6bd1bc803276930e0ecda12b1f4db803c1831f8 Mon Sep 17 00:00:00 2001 From: Terrence Keane Date: Tue, 23 Sep 2025 13:48:52 -0400 Subject: [PATCH 01/55] feat(SearchForm): add SearchForm component --- packages/code-editor/src/SearchForm/SearchForm.spec.tsx | 6 ++++++ packages/code-editor/src/SearchForm/SearchForm.stories.tsx | 0 packages/code-editor/src/SearchForm/SearchForm.styles.ts | 0 packages/code-editor/src/SearchForm/SearchForm.tsx | 7 +++++++ packages/code-editor/src/SearchForm/SearchForm.types.ts | 5 +++++ packages/code-editor/src/SearchForm/index.ts | 2 ++ 6 files changed, 20 insertions(+) create mode 100644 packages/code-editor/src/SearchForm/SearchForm.spec.tsx create mode 100644 packages/code-editor/src/SearchForm/SearchForm.stories.tsx create mode 100644 packages/code-editor/src/SearchForm/SearchForm.styles.ts create mode 100644 packages/code-editor/src/SearchForm/SearchForm.tsx create mode 100644 packages/code-editor/src/SearchForm/SearchForm.types.ts create mode 100644 packages/code-editor/src/SearchForm/index.ts diff --git a/packages/code-editor/src/SearchForm/SearchForm.spec.tsx b/packages/code-editor/src/SearchForm/SearchForm.spec.tsx new file mode 100644 index 0000000000..7c9ae62d46 --- /dev/null +++ b/packages/code-editor/src/SearchForm/SearchForm.spec.tsx @@ -0,0 +1,6 @@ +import React from 'react'; +import { render } from '@testing-library/react'; + +import { SearchForm } from './SearchForm'; + +describe('SearchForm', () => {}); diff --git a/packages/code-editor/src/SearchForm/SearchForm.stories.tsx b/packages/code-editor/src/SearchForm/SearchForm.stories.tsx new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/code-editor/src/SearchForm/SearchForm.styles.ts b/packages/code-editor/src/SearchForm/SearchForm.styles.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/code-editor/src/SearchForm/SearchForm.tsx b/packages/code-editor/src/SearchForm/SearchForm.tsx new file mode 100644 index 0000000000..befbf1c4fe --- /dev/null +++ b/packages/code-editor/src/SearchForm/SearchForm.tsx @@ -0,0 +1,7 @@ +import React from 'react'; + +import { SearchFormProps } from './SearchForm.types'; + +export function SearchForm({ view }: SearchFormProps) { + return
SearchForm
; +} diff --git a/packages/code-editor/src/SearchForm/SearchForm.types.ts b/packages/code-editor/src/SearchForm/SearchForm.types.ts new file mode 100644 index 0000000000..689eb79c85 --- /dev/null +++ b/packages/code-editor/src/SearchForm/SearchForm.types.ts @@ -0,0 +1,5 @@ +import { CodeMirrorView } from '../CodeEditor'; + +export interface SearchFormProps { + view: CodeMirrorView; +} diff --git a/packages/code-editor/src/SearchForm/index.ts b/packages/code-editor/src/SearchForm/index.ts new file mode 100644 index 0000000000..973afeceba --- /dev/null +++ b/packages/code-editor/src/SearchForm/index.ts @@ -0,0 +1,2 @@ +export { SearchForm } from './SearchForm'; +export { type SearchFormProps } from './SearchForm.types'; From 58544a57a5de698ccb23b6b5b204fe676872ba1a Mon Sep 17 00:00:00 2001 From: Terrence Keane Date: Wed, 24 Sep 2025 12:19:36 -0400 Subject: [PATCH 02/55] feat(SearchForm): implement SearchForm component with toggle functionality and styling - Added the SearchForm component to the CodeEditor, featuring a toggle button for expanding and collapsing the search input. - Integrated LeafyGreen UI components for consistent styling and functionality. - Created a new story for SearchForm in Storybook to demonstrate its usage and appearance. - Updated CodeEditor to include the SearchForm, enhancing user interaction capabilities. --- packages/code-editor/package.json | 1 + .../code-editor/src/CodeEditor/CodeEditor.tsx | 15 +++- .../src/SearchForm/SearchForm.stories.tsx | 55 +++++++++++++++ .../src/SearchForm/SearchForm.styles.ts | 59 ++++++++++++++++ .../code-editor/src/SearchForm/SearchForm.tsx | 70 ++++++++++++++++++- packages/code-editor/tsconfig.json | 3 + pnpm-lock.yaml | 3 + 7 files changed, 202 insertions(+), 4 deletions(-) diff --git a/packages/code-editor/package.json b/packages/code-editor/package.json index f15910821e..5374af1bf6 100644 --- a/packages/code-editor/package.json +++ b/packages/code-editor/package.json @@ -57,6 +57,7 @@ "@leafygreen-ui/menu": "workspace:^", "@leafygreen-ui/modal": "workspace:^", "@leafygreen-ui/palette": "workspace:^", + "@leafygreen-ui/text-input": "workspace:^", "@leafygreen-ui/tokens": "workspace:^", "@leafygreen-ui/tooltip": "workspace:^", "@leafygreen-ui/typography": "workspace:^", diff --git a/packages/code-editor/src/CodeEditor/CodeEditor.tsx b/packages/code-editor/src/CodeEditor/CodeEditor.tsx index 868cbfbd8c..9fd12551f1 100644 --- a/packages/code-editor/src/CodeEditor/CodeEditor.tsx +++ b/packages/code-editor/src/CodeEditor/CodeEditor.tsx @@ -6,6 +6,7 @@ import React, { useRef, useState, } from 'react'; +import { renderToString } from 'react-dom/server'; import { type EditorView, type ViewUpdate } from '@codemirror/view'; import { useDarkMode } from '@leafygreen-ui/leafygreen-provider'; @@ -15,7 +16,7 @@ import { Body, useUpdatedBaseFontSize } from '@leafygreen-ui/typography'; import { CodeEditorContextMenu } from '../CodeEditorContextMenu'; import { CodeEditorCopyButton } from '../CodeEditorCopyButton'; import { CopyButtonVariant } from '../CodeEditorCopyButton/CodeEditorCopyButton.types'; -import { Panel as CodeEditorPanel } from '../Panel'; +import { SearchForm } from '../SearchForm'; import { getLgIds } from '../utils'; import { useModules } from './hooks/useModules'; @@ -271,7 +272,17 @@ const BaseCodeEditor = forwardRef( ), commands.history(), - searchModule.search(), + searchModule.search({ + createPanel: view => { + const dom = document.createElement('div'); + dom.innerHTML = renderToString( + React.createElement(SearchForm, { + view, + }), + ); + return { dom, top: true }; + }, + }), EditorView.EditorView.updateListener.of((update: ViewUpdate) => { if (isControlled && update.docChanged) { diff --git a/packages/code-editor/src/SearchForm/SearchForm.stories.tsx b/packages/code-editor/src/SearchForm/SearchForm.stories.tsx index e69de29bb2..030e117421 100644 --- a/packages/code-editor/src/SearchForm/SearchForm.stories.tsx +++ b/packages/code-editor/src/SearchForm/SearchForm.stories.tsx @@ -0,0 +1,55 @@ +import React from 'react'; +import { + storybookArgTypes, + storybookExcludedControlParams, + StoryMetaType, +} from '@lg-tools/storybook-utils'; +import type { StoryFn } from '@storybook/react'; + +import LeafyGreenProvider from '@leafygreen-ui/leafygreen-provider'; +import { BaseFontSize } from '@leafygreen-ui/tokens'; + +import { SearchForm } from './SearchForm'; + +const Root = (Story: StoryFn, context: any) => ( + + + +); + +const meta: StoryMetaType = { + title: 'Components/Inputs/CodeEditor/SearchForm', + component: SearchForm, + parameters: { + default: 'LiveExample', + controls: { + exclude: [...storybookExcludedControlParams, 'extensions'], + }, + generate: { + combineArgs: { + darkMode: [false, true], + baseFontSize: Object.values(BaseFontSize), + }, + decorator: Root, + }, + }, + decorators: [Root], + args: { + baseFontSize: BaseFontSize.Body1, + darkMode: false, + }, + argTypes: { + darkMode: storybookArgTypes.darkMode, + baseFontSize: storybookArgTypes.updatedBaseFontSize, + }, +}; + +export default meta; + +const Template: StoryFn = args => ; + +export const LiveExample = Template.bind({}); +export const Generated = () => {}; diff --git a/packages/code-editor/src/SearchForm/SearchForm.styles.ts b/packages/code-editor/src/SearchForm/SearchForm.styles.ts index e69de29bb2..0b722ea1db 100644 --- a/packages/code-editor/src/SearchForm/SearchForm.styles.ts +++ b/packages/code-editor/src/SearchForm/SearchForm.styles.ts @@ -0,0 +1,59 @@ +import { css, cx } from '@leafygreen-ui/emotion'; +import { Theme } from '@leafygreen-ui/lib'; +import { + borderRadius, + InteractionState, + shadow, + spacing, + transitionDuration, + Variant, +} from '@leafygreen-ui/tokens'; +import { color } from '@leafygreen-ui/tokens'; + +const CONTAINER_MAX_WIDTH = 500; + +export const getSearchFormContainerStyles = (theme: Theme) => css` + background-color: ${color[theme].background[Variant.Secondary][ + InteractionState.Default + ]}; + border-bottom-left-radius: ${borderRadius[150]}px; + border-bottom-right-radius: ${borderRadius[150]}px; + box-shadow: ${shadow[theme][100]}; + max-width: ${CONTAINER_MAX_WIDTH}px; + width: 100%; +`; + +export const findSectionStyles = css` + display: grid; + grid-template-columns: auto 1fr repeat(4, auto); + align-items: center; + gap: 0 ${spacing[100]}px; + margin: 8px 10px 8px; +`; + +export const toggleIconStyles = css` + transform: rotate(-180deg); + transition: transform ${transitionDuration.slower}ms ease-in-out; +`; + +export const openToggleIconStyles = css` + transform: rotate(0deg); +`; + +export const toggleButtonStyles = css``; + +export const getIconStyles = (isOpen: boolean) => + cx(toggleIconStyles, { + [openToggleIconStyles]: isOpen, + }); + +export const findInputContainerStyles = css` + position: relative; + /* width: 240px; */ +`; + +export const findInputIconButtonStyles = css` + position: absolute; + right: ${spacing[100]}px; + top: ${spacing[100]}px; +`; diff --git a/packages/code-editor/src/SearchForm/SearchForm.tsx b/packages/code-editor/src/SearchForm/SearchForm.tsx index befbf1c4fe..f9ba99eb66 100644 --- a/packages/code-editor/src/SearchForm/SearchForm.tsx +++ b/packages/code-editor/src/SearchForm/SearchForm.tsx @@ -1,7 +1,73 @@ -import React from 'react'; +import React, { MouseEvent, useCallback, useState } from 'react'; +import Button from '@leafygreen-ui/button'; +import IconButton from '@leafygreen-ui/icon-button'; +import { useDarkMode } from '@leafygreen-ui/leafygreen-provider'; +import TextInput from '@leafygreen-ui/text-input'; + +import { Icon } from '../../../icon/src/Icon'; + +import { + findInputContainerStyles, + findInputIconButtonStyles, + findSectionStyles, + getIconStyles, + getSearchFormContainerStyles, +} from './SearchForm.styles'; import { SearchFormProps } from './SearchForm.types'; export function SearchForm({ view }: SearchFormProps) { - return
SearchForm
; + const [isOpen, setIsOpen] = useState(false); + const { theme } = useDarkMode(); + + const handleToggleButtonClick = useCallback( + (e: MouseEvent) => { + setIsOpen(currState => !currState); + }, + [], + ); + + return ( +
+
+ + + +
+ + + + +
+ + + + + + + + + + +
+
+ ); } diff --git a/packages/code-editor/tsconfig.json b/packages/code-editor/tsconfig.json index 3ca0966d1f..31d3645370 100644 --- a/packages/code-editor/tsconfig.json +++ b/packages/code-editor/tsconfig.json @@ -39,6 +39,9 @@ { "path": "../tokens" }, + { + "path": "../text-input" + }, { "path": "../leafygreen-provider" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d4cb728d57..0847d79df6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1323,6 +1323,9 @@ importers: '@leafygreen-ui/palette': specifier: workspace:^ version: link:../palette + '@leafygreen-ui/text-input': + specifier: workspace:^ + version: link:../text-input '@leafygreen-ui/tokens': specifier: workspace:^ version: link:../tokens From 56e3da156fe4ce06c325e7b11cfd7d16f4c852f4 Mon Sep 17 00:00:00 2001 From: Terrence Keane Date: Wed, 24 Sep 2025 15:20:06 -0400 Subject: [PATCH 03/55] refactor(CodeEditor): add back in panel import --- packages/code-editor/src/CodeEditor/CodeEditor.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/code-editor/src/CodeEditor/CodeEditor.tsx b/packages/code-editor/src/CodeEditor/CodeEditor.tsx index 9fd12551f1..6a71754e11 100644 --- a/packages/code-editor/src/CodeEditor/CodeEditor.tsx +++ b/packages/code-editor/src/CodeEditor/CodeEditor.tsx @@ -16,6 +16,7 @@ import { Body, useUpdatedBaseFontSize } from '@leafygreen-ui/typography'; import { CodeEditorContextMenu } from '../CodeEditorContextMenu'; import { CodeEditorCopyButton } from '../CodeEditorCopyButton'; import { CopyButtonVariant } from '../CodeEditorCopyButton/CodeEditorCopyButton.types'; +import { Panel as CodeEditorPanel } from '../Panel'; import { SearchForm } from '../SearchForm'; import { getLgIds } from '../utils'; From 5eded55972c864ec9f770cc629e00e89689c1fe0 Mon Sep 17 00:00:00 2001 From: Terrence Keane Date: Wed, 24 Sep 2025 16:00:23 -0400 Subject: [PATCH 04/55] refactor(SearchForm): further correct styling --- packages/code-editor/src/CodeEditor/CodeEditor.tsx | 8 ++++++-- packages/code-editor/src/CodeEditor/CodeEditor.types.ts | 4 +++- .../src/CodeEditor/hooks/extensions/useThemeExtension.ts | 8 ++++++++ packages/code-editor/src/SearchForm/SearchForm.styles.ts | 3 +-- 4 files changed, 18 insertions(+), 5 deletions(-) diff --git a/packages/code-editor/src/CodeEditor/CodeEditor.tsx b/packages/code-editor/src/CodeEditor/CodeEditor.tsx index 6a71754e11..68d404e5cf 100644 --- a/packages/code-editor/src/CodeEditor/CodeEditor.tsx +++ b/packages/code-editor/src/CodeEditor/CodeEditor.tsx @@ -6,7 +6,7 @@ import React, { useRef, useState, } from 'react'; -import { renderToString } from 'react-dom/server'; +import { createRoot } from 'react-dom/client'; import { type EditorView, type ViewUpdate } from '@codemirror/view'; import { useDarkMode } from '@leafygreen-ui/leafygreen-provider'; @@ -276,7 +276,11 @@ const BaseCodeEditor = forwardRef( searchModule.search({ createPanel: view => { const dom = document.createElement('div'); - dom.innerHTML = renderToString( + dom.style.position = 'absolute'; + dom.style.top = '-8px'; // Accounts for top padding of the editor + dom.style.right = '0'; + + createRoot(dom).render( React.createElement(SearchForm, { view, }), diff --git a/packages/code-editor/src/CodeEditor/CodeEditor.types.ts b/packages/code-editor/src/CodeEditor/CodeEditor.types.ts index 4c13844254..303fba3b81 100644 --- a/packages/code-editor/src/CodeEditor/CodeEditor.types.ts +++ b/packages/code-editor/src/CodeEditor/CodeEditor.types.ts @@ -96,7 +96,9 @@ export const CodeEditorSelectors = { LineWrapping: '.cm-lineWrapping', SearchInput: 'input[type="text"], .cm-textfield, input[placeholder*="search" i]', - SearchPanel: '.cm-search, .cm-panel', + SearchPanel: '.cm-panel', + SearchPanelContainer: '.cm-panels', + SearchPanelContainerTop: '.cm-panels-top', SelectionBackground: '.cm-selectionBackground', Tooltip: '.cm-tooltip', } as const; diff --git a/packages/code-editor/src/CodeEditor/hooks/extensions/useThemeExtension.ts b/packages/code-editor/src/CodeEditor/hooks/extensions/useThemeExtension.ts index ad111eeaae..2500e62c44 100644 --- a/packages/code-editor/src/CodeEditor/hooks/extensions/useThemeExtension.ts +++ b/packages/code-editor/src/CodeEditor/hooks/extensions/useThemeExtension.ts @@ -140,6 +140,14 @@ export function useThemeExtension({ [CodeEditorSelectors.DiagnosticInfo]: { border: 'none', }, + + [CodeEditorSelectors.SearchPanelContainer]: { + backgroundColor: 'transparent', + }, + + [CodeEditorSelectors.SearchPanelContainerTop]: { + border: 'none', + }, }, { dark: theme === Theme.Dark }, ); diff --git a/packages/code-editor/src/SearchForm/SearchForm.styles.ts b/packages/code-editor/src/SearchForm/SearchForm.styles.ts index 0b722ea1db..54fafc70c2 100644 --- a/packages/code-editor/src/SearchForm/SearchForm.styles.ts +++ b/packages/code-editor/src/SearchForm/SearchForm.styles.ts @@ -28,7 +28,7 @@ export const findSectionStyles = css` grid-template-columns: auto 1fr repeat(4, auto); align-items: center; gap: 0 ${spacing[100]}px; - margin: 8px 10px 8px; + padding: 8px 10px 8px; `; export const toggleIconStyles = css` @@ -49,7 +49,6 @@ export const getIconStyles = (isOpen: boolean) => export const findInputContainerStyles = css` position: relative; - /* width: 240px; */ `; export const findInputIconButtonStyles = css` From d9783330bd8d03e87396a110569567daf1a60bd8 Mon Sep 17 00:00:00 2001 From: Terrence Keane Date: Wed, 24 Sep 2025 18:48:46 -0400 Subject: [PATCH 05/55] WIP --- .../code-editor/src/SearchForm/SearchForm.tsx | 49 +++++++++++++++++-- 1 file changed, 46 insertions(+), 3 deletions(-) diff --git a/packages/code-editor/src/SearchForm/SearchForm.tsx b/packages/code-editor/src/SearchForm/SearchForm.tsx index f9ba99eb66..11aa94fde4 100644 --- a/packages/code-editor/src/SearchForm/SearchForm.tsx +++ b/packages/code-editor/src/SearchForm/SearchForm.tsx @@ -1,4 +1,14 @@ -import React, { MouseEvent, useCallback, useState } from 'react'; +import React, { ChangeEvent, MouseEvent, useCallback, useState } from 'react'; +import { + closeSearchPanel, + findNext, + findPrevious, + replaceAll, + replaceNext, + SearchQuery, + selectMatches, + setSearchQuery, +} from '@codemirror/search'; import Button from '@leafygreen-ui/button'; import IconButton from '@leafygreen-ui/icon-button'; @@ -27,6 +37,33 @@ export function SearchForm({ view }: SearchFormProps) { [], ); + const handleCloseButtonClick = useCallback( + (e: MouseEvent) => { + closeSearchPanel(view); + }, + [view], + ); + + const handleFindInputChange = useCallback( + (e: ChangeEvent) => { + const newQuery = new SearchQuery({ + search: 'test', + caseSensitive: true, + regexp: false, + wholeWord: false, + replace: '', + }); + + view.dispatch({ effects: setSearchQuery.of(newQuery) }); + + // if (!query || !query.eq(newQuery)) { + // setQuery(newQuery); + // view.dispatch({ effects: setSearchQuery.of(newQuery) }); + // } + }, + [view], + ); + return (
@@ -39,7 +76,13 @@ export function SearchForm({ view }: SearchFormProps) {
- + All From fa6c306e697f3b571c436c0e5c32fbc6dd08bb6e Mon Sep 17 00:00:00 2001 From: Terrence Keane Date: Thu, 25 Sep 2025 14:53:19 -0400 Subject: [PATCH 06/55] refactor(SearchForm): Further correct styling --- .../src/SearchForm/SearchForm.styles.ts | 77 ++++++++++++++++--- .../code-editor/src/SearchForm/SearchForm.tsx | 64 +++++++++------ 2 files changed, 106 insertions(+), 35 deletions(-) diff --git a/packages/code-editor/src/SearchForm/SearchForm.styles.ts b/packages/code-editor/src/SearchForm/SearchForm.styles.ts index 54fafc70c2..d028715427 100644 --- a/packages/code-editor/src/SearchForm/SearchForm.styles.ts +++ b/packages/code-editor/src/SearchForm/SearchForm.styles.ts @@ -11,8 +11,9 @@ import { import { color } from '@leafygreen-ui/tokens'; const CONTAINER_MAX_WIDTH = 500; +const INPUT_WIDTH = 240; -export const getSearchFormContainerStyles = (theme: Theme) => css` +const getBaseContainerStyles = (theme: Theme) => css` background-color: ${color[theme].background[Variant.Secondary][ InteractionState.Default ]}; @@ -21,34 +22,81 @@ export const getSearchFormContainerStyles = (theme: Theme) => css` box-shadow: ${shadow[theme][100]}; max-width: ${CONTAINER_MAX_WIDTH}px; width: 100%; + display: grid; + grid-template-rows: 52px 0fr; + overflow: hidden; + transition: grid-template-rows ${transitionDuration.slower}ms ease-in-out; +`; + +const openContainerStyles = css` + grid-template-rows: 52px 1fr; `; +export const getContainerStyles = ({ + theme, + isOpen, +}: { + theme: Theme; + isOpen: boolean; +}) => + cx(getBaseContainerStyles(theme), { + [openContainerStyles]: isOpen, + }); + export const findSectionStyles = css` - display: grid; - grid-template-columns: auto 1fr repeat(4, auto); + display: flex; align-items: center; - gap: 0 ${spacing[100]}px; - padding: 8px 10px 8px; + padding: ${spacing[200]}px ${spacing[300]}px; + height: 100%; `; -export const toggleIconStyles = css` +export const replaceSectionStyles = css` + min-height: 0; + overflow: hidden; +`; + +/** + * Inner section used for padding and border so that the outer section can + * fully close to 0px when set to 0fr. + */ +export const getReplaceInnerSectionStyles = (theme: Theme) => css` + display: flex; + align-items: center; + padding: 8px 10px 8px 44px; + border-top: 1px solid + ${color[theme].border[Variant.Secondary][InteractionState.Default]}; +`; + +export const toggleButtonStyles = css` + margin-right: ${spacing[100]}px; +`; + +const toggleIconStyles = css` transform: rotate(-180deg); transition: transform ${transitionDuration.slower}ms ease-in-out; `; -export const openToggleIconStyles = css` +const openToggleIconStyles = css` transform: rotate(0deg); `; -export const toggleButtonStyles = css``; - -export const getIconStyles = (isOpen: boolean) => +export const getToggleIconStyles = (isOpen: boolean) => cx(toggleIconStyles, { [openToggleIconStyles]: isOpen, }); export const findInputContainerStyles = css` position: relative; + width: ${INPUT_WIDTH}px; + margin-right: ${spacing[100]}px; +`; + +export const allButtonStyles = css` + margin-left: ${spacing[100]}px; +`; + +export const closeButtonStyles = css` + margin-left: auto; `; export const findInputIconButtonStyles = css` @@ -56,3 +104,12 @@ export const findInputIconButtonStyles = css` right: ${spacing[100]}px; top: ${spacing[100]}px; `; + +export const replaceInputContainerStyles = css` + position: relative; + width: ${INPUT_WIDTH}px; +`; + +export const replaceButtonStyles = css` + margin-left: ${spacing[100]}px; +`; diff --git a/packages/code-editor/src/SearchForm/SearchForm.tsx b/packages/code-editor/src/SearchForm/SearchForm.tsx index 11aa94fde4..45eeb5b6b5 100644 --- a/packages/code-editor/src/SearchForm/SearchForm.tsx +++ b/packages/code-editor/src/SearchForm/SearchForm.tsx @@ -18,11 +18,18 @@ import TextInput from '@leafygreen-ui/text-input'; import { Icon } from '../../../icon/src/Icon'; import { + allButtonStyles, + closeButtonStyles, findInputContainerStyles, findInputIconButtonStyles, findSectionStyles, - getIconStyles, - getSearchFormContainerStyles, + getContainerStyles, + getReplaceInnerSectionStyles, + getToggleIconStyles, + replaceButtonStyles, + replaceInputContainerStyles, + replaceSectionStyles, + toggleButtonStyles, } from './SearchForm.styles'; import { SearchFormProps } from './SearchForm.types'; @@ -46,16 +53,14 @@ export function SearchForm({ view }: SearchFormProps) { const handleFindInputChange = useCallback( (e: ChangeEvent) => { - const newQuery = new SearchQuery({ - search: 'test', - caseSensitive: true, - regexp: false, - wholeWord: false, - replace: '', - }); - - view.dispatch({ effects: setSearchQuery.of(newQuery) }); - + // const newQuery = new SearchQuery({ + // search: 'test', + // caseSensitive: true, + // regexp: false, + // wholeWord: false, + // replace: '', + // }); + // view.dispatch({ effects: setSearchQuery.of(newQuery) }); // if (!query || !query.eq(newQuery)) { // setQuery(newQuery); // view.dispatch({ effects: setSearchQuery.of(newQuery) }); @@ -65,15 +70,15 @@ export function SearchForm({ view }: SearchFormProps) { ); return ( -
+
- +
- + - + - +
+
+
+ + + +
+
); } From e8303124447b71db1a30b58d19401ef63c6b9fd8 Mon Sep 17 00:00:00 2001 From: Terrence Keane Date: Thu, 25 Sep 2025 14:57:38 -0400 Subject: [PATCH 07/55] make top row height more dynamic --- packages/code-editor/src/SearchForm/SearchForm.styles.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/code-editor/src/SearchForm/SearchForm.styles.ts b/packages/code-editor/src/SearchForm/SearchForm.styles.ts index d028715427..89cf18716f 100644 --- a/packages/code-editor/src/SearchForm/SearchForm.styles.ts +++ b/packages/code-editor/src/SearchForm/SearchForm.styles.ts @@ -23,13 +23,13 @@ const getBaseContainerStyles = (theme: Theme) => css` max-width: ${CONTAINER_MAX_WIDTH}px; width: 100%; display: grid; - grid-template-rows: 52px 0fr; + grid-template-rows: 1fr 0fr; overflow: hidden; transition: grid-template-rows ${transitionDuration.slower}ms ease-in-out; `; const openContainerStyles = css` - grid-template-rows: 52px 1fr; + grid-template-rows: 1fr 1fr; `; export const getContainerStyles = ({ From 933b80de76f16ee5ad54b42b082496c9c224b3d1 Mon Sep 17 00:00:00 2001 From: Terrence Keane Date: Thu, 25 Sep 2025 16:00:28 -0400 Subject: [PATCH 08/55] fix top section height --- packages/code-editor/src/SearchForm/SearchForm.styles.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/code-editor/src/SearchForm/SearchForm.styles.ts b/packages/code-editor/src/SearchForm/SearchForm.styles.ts index 89cf18716f..27b45d06c9 100644 --- a/packages/code-editor/src/SearchForm/SearchForm.styles.ts +++ b/packages/code-editor/src/SearchForm/SearchForm.styles.ts @@ -11,6 +11,7 @@ import { import { color } from '@leafygreen-ui/tokens'; const CONTAINER_MAX_WIDTH = 500; +const SECTION_HEIGHT = 52; const INPUT_WIDTH = 240; const getBaseContainerStyles = (theme: Theme) => css` @@ -23,13 +24,13 @@ const getBaseContainerStyles = (theme: Theme) => css` max-width: ${CONTAINER_MAX_WIDTH}px; width: 100%; display: grid; - grid-template-rows: 1fr 0fr; + grid-template-rows: ${SECTION_HEIGHT}px 0fr; overflow: hidden; transition: grid-template-rows ${transitionDuration.slower}ms ease-in-out; `; const openContainerStyles = css` - grid-template-rows: 1fr 1fr; + grid-template-rows: ${SECTION_HEIGHT}px 1fr; `; export const getContainerStyles = ({ From 907cbb4d2c00bed2d2dfbdbe578d023d89e8cdf8 Mon Sep 17 00:00:00 2001 From: Terrence Keane Date: Fri, 26 Sep 2025 15:35:30 -0400 Subject: [PATCH 09/55] WIP --- .../src/SearchForm/SearchForm.styles.ts | 11 ++++++++-- .../code-editor/src/SearchForm/SearchForm.tsx | 21 +++++++------------ 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/packages/code-editor/src/SearchForm/SearchForm.styles.ts b/packages/code-editor/src/SearchForm/SearchForm.styles.ts index 27b45d06c9..173c721fbf 100644 --- a/packages/code-editor/src/SearchForm/SearchForm.styles.ts +++ b/packages/code-editor/src/SearchForm/SearchForm.styles.ts @@ -88,7 +88,8 @@ export const getToggleIconStyles = (isOpen: boolean) => export const findInputContainerStyles = css` position: relative; - width: ${INPUT_WIDTH}px; + flex: 1 1 ${INPUT_WIDTH}px; + min-width: 100px; margin-right: ${spacing[100]}px; `; @@ -108,9 +109,15 @@ export const findInputIconButtonStyles = css` export const replaceInputContainerStyles = css` position: relative; - width: ${INPUT_WIDTH}px; + flex: 1 1 ${INPUT_WIDTH}px; + min-width: 100px; + width: 100%; `; export const replaceButtonStyles = css` margin-left: ${spacing[100]}px; `; + +export const findInputStyles = css` + width: 100%; +`; diff --git a/packages/code-editor/src/SearchForm/SearchForm.tsx b/packages/code-editor/src/SearchForm/SearchForm.tsx index 45eeb5b6b5..dbf57cc0bc 100644 --- a/packages/code-editor/src/SearchForm/SearchForm.tsx +++ b/packages/code-editor/src/SearchForm/SearchForm.tsx @@ -1,14 +1,5 @@ import React, { ChangeEvent, MouseEvent, useCallback, useState } from 'react'; -import { - closeSearchPanel, - findNext, - findPrevious, - replaceAll, - replaceNext, - SearchQuery, - selectMatches, - setSearchQuery, -} from '@codemirror/search'; +import { closeSearchPanel } from '@codemirror/search'; import Button from '@leafygreen-ui/button'; import IconButton from '@leafygreen-ui/icon-button'; @@ -22,6 +13,7 @@ import { closeButtonStyles, findInputContainerStyles, findInputIconButtonStyles, + findInputStyles, findSectionStyles, getContainerStyles, getReplaceInnerSectionStyles, @@ -38,21 +30,21 @@ export function SearchForm({ view }: SearchFormProps) { const { theme } = useDarkMode(); const handleToggleButtonClick = useCallback( - (e: MouseEvent) => { + (_e: MouseEvent) => { setIsOpen(currState => !currState); }, [], ); const handleCloseButtonClick = useCallback( - (e: MouseEvent) => { + (_e: MouseEvent) => { closeSearchPanel(view); }, [view], ); const handleFindInputChange = useCallback( - (e: ChangeEvent) => { + (_e: ChangeEvent) => { // const newQuery = new SearchQuery({ // search: 'test', // caseSensitive: true, @@ -66,7 +58,7 @@ export function SearchForm({ view }: SearchFormProps) { // view.dispatch({ effects: setSearchQuery.of(newQuery) }); // } }, - [view], + [], ); return ( @@ -85,6 +77,7 @@ export function SearchForm({ view }: SearchFormProps) { placeholder="Find" aria-labelledby="find" onChange={handleFindInputChange} + className={findInputStyles} // eslint-disable-next-line jsx-a11y/no-autofocus autoFocus /> From a498900def70b444610779920e1e813cb4b7550f Mon Sep 17 00:00:00 2001 From: Terrence Keane Date: Mon, 29 Sep 2025 19:00:36 -0400 Subject: [PATCH 10/55] Fix width styling --- .../code-editor/src/CodeEditor/CodeEditor.tsx | 3 +++ .../src/SearchForm/SearchForm.styles.ts | 4 +++- .../code-editor/src/SearchForm/SearchForm.tsx | 17 ++++++++++++++--- 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/packages/code-editor/src/CodeEditor/CodeEditor.tsx b/packages/code-editor/src/CodeEditor/CodeEditor.tsx index 68d404e5cf..0654f88aba 100644 --- a/packages/code-editor/src/CodeEditor/CodeEditor.tsx +++ b/packages/code-editor/src/CodeEditor/CodeEditor.tsx @@ -279,6 +279,9 @@ const BaseCodeEditor = forwardRef( dom.style.position = 'absolute'; dom.style.top = '-8px'; // Accounts for top padding of the editor dom.style.right = '0'; + dom.style.left = '0'; + dom.style.display = 'flex'; + dom.style.justifyContent = 'flex-end'; createRoot(dom).render( React.createElement(SearchForm, { diff --git a/packages/code-editor/src/SearchForm/SearchForm.styles.ts b/packages/code-editor/src/SearchForm/SearchForm.styles.ts index 173c721fbf..8e492751c4 100644 --- a/packages/code-editor/src/SearchForm/SearchForm.styles.ts +++ b/packages/code-editor/src/SearchForm/SearchForm.styles.ts @@ -21,8 +21,8 @@ const getBaseContainerStyles = (theme: Theme) => css` border-bottom-left-radius: ${borderRadius[150]}px; border-bottom-right-radius: ${borderRadius[150]}px; box-shadow: ${shadow[theme][100]}; - max-width: ${CONTAINER_MAX_WIDTH}px; width: 100%; + max-width: ${CONTAINER_MAX_WIDTH}px; display: grid; grid-template-rows: ${SECTION_HEIGHT}px 0fr; overflow: hidden; @@ -90,6 +90,7 @@ export const findInputContainerStyles = css` position: relative; flex: 1 1 ${INPUT_WIDTH}px; min-width: 100px; + max-width: ${INPUT_WIDTH}px; margin-right: ${spacing[100]}px; `; @@ -111,6 +112,7 @@ export const replaceInputContainerStyles = css` position: relative; flex: 1 1 ${INPUT_WIDTH}px; min-width: 100px; + max-width: ${INPUT_WIDTH}px; width: 100%; `; diff --git a/packages/code-editor/src/SearchForm/SearchForm.tsx b/packages/code-editor/src/SearchForm/SearchForm.tsx index dbf57cc0bc..c7419f6476 100644 --- a/packages/code-editor/src/SearchForm/SearchForm.tsx +++ b/packages/code-editor/src/SearchForm/SearchForm.tsx @@ -62,7 +62,10 @@ export function SearchForm({ view }: SearchFormProps) { ); return ( -
+
-
+
Replace -
From 3f9b74a9dc945208327aefe7fc353f8f875ca745 Mon Sep 17 00:00:00 2001 From: Terrence Keane Date: Mon, 29 Sep 2025 20:12:30 -0400 Subject: [PATCH 11/55] Fix box shadow --- .../src/SearchForm/SearchForm.styles.ts | 29 +++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/packages/code-editor/src/SearchForm/SearchForm.styles.ts b/packages/code-editor/src/SearchForm/SearchForm.styles.ts index 8e492751c4..fa139f2b56 100644 --- a/packages/code-editor/src/SearchForm/SearchForm.styles.ts +++ b/packages/code-editor/src/SearchForm/SearchForm.styles.ts @@ -20,13 +20,38 @@ const getBaseContainerStyles = (theme: Theme) => css` ]}; border-bottom-left-radius: ${borderRadius[150]}px; border-bottom-right-radius: ${borderRadius[150]}px; - box-shadow: ${shadow[theme][100]}; width: 100%; max-width: ${CONTAINER_MAX_WIDTH}px; + position: relative; display: grid; grid-template-rows: ${SECTION_HEIGHT}px 0fr; - overflow: hidden; transition: grid-template-rows ${transitionDuration.slower}ms ease-in-out; + z-index: 1; + + /** Add a shadow to the container while clipping the right edge*/ + &::after { + content: ''; + + /** Position the pseudo-element to match the parent's size and location */ + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + + /** Apply the shadow to the pseudo-element */ + box-shadow: ${shadow[theme][100]}; + + /** Negative values expand the clipping area outward, revealing the shadow. */ + /** A zero value clips the shadow exactly at the element's edge. */ + clip-path: inset(-20px 0px -20px -20px); + + /** Match the parent's border-radius for consistency */ + border-radius: inherit; + + /** Places the pseudo-element behind the parent element */ + z-index: -1; + } `; const openContainerStyles = css` From f90417146b43e9e899e2342db722cd2430b6423c Mon Sep 17 00:00:00 2001 From: Terrence Keane Date: Mon, 29 Sep 2025 20:15:42 -0400 Subject: [PATCH 12/55] Add find functionality --- .../code-editor/src/SearchForm/SearchForm.tsx | 106 +++++++++++++----- 1 file changed, 80 insertions(+), 26 deletions(-) diff --git a/packages/code-editor/src/SearchForm/SearchForm.tsx b/packages/code-editor/src/SearchForm/SearchForm.tsx index c7419f6476..f73dcd4cce 100644 --- a/packages/code-editor/src/SearchForm/SearchForm.tsx +++ b/packages/code-editor/src/SearchForm/SearchForm.tsx @@ -1,5 +1,19 @@ -import React, { ChangeEvent, MouseEvent, useCallback, useState } from 'react'; -import { closeSearchPanel } from '@codemirror/search'; +import React, { + ChangeEvent, + FormEvent, + MouseEvent, + useCallback, + useEffect, + useState, +} from 'react'; +import { + closeSearchPanel, + findNext, + findPrevious, + SearchQuery, + selectMatches, + setSearchQuery, +} from '@codemirror/search'; import Button from '@leafygreen-ui/button'; import IconButton from '@leafygreen-ui/icon-button'; @@ -27,6 +41,8 @@ import { SearchFormProps } from './SearchForm.types'; export function SearchForm({ view }: SearchFormProps) { const [isOpen, setIsOpen] = useState(false); + const [searchString, setSearchString] = useState(''); + const [findCount, setFindCount] = useState(0); const { theme } = useDarkMode(); const handleToggleButtonClick = useCallback( @@ -43,24 +59,45 @@ export function SearchForm({ view }: SearchFormProps) { [view], ); - const handleFindInputChange = useCallback( + const handleSearchQueryChange = useCallback( (_e: ChangeEvent) => { - // const newQuery = new SearchQuery({ - // search: 'test', - // caseSensitive: true, - // regexp: false, - // wholeWord: false, - // replace: '', - // }); - // view.dispatch({ effects: setSearchQuery.of(newQuery) }); - // if (!query || !query.eq(newQuery)) { - // setQuery(newQuery); - // view.dispatch({ effects: setSearchQuery.of(newQuery) }); - // } + setSearchString(_e.target.value); }, [], ); + useEffect(() => { + const query = new SearchQuery({ + search: searchString, + caseSensitive: true, + regexp: false, + wholeWord: false, + replace: '', + }); + + view.dispatch({ effects: setSearchQuery.of(query) }); + + const cursor = query.getCursor(view.state.doc); + let count = 0; + + let result = cursor.next(); + + while (!result.done) { + count++; + result = cursor.next(); + } + + setFindCount(count); + }, [searchString, view]); + + const handleFindFormSubmit = useCallback( + (e: FormEvent) => { + e.preventDefault(); + findNext(view); + }, + [view], + ); + return (
- +
+ +
- + findPrevious(view)} + > - + findNext(view)} + > - + Date: Mon, 29 Sep 2025 20:25:10 -0400 Subject: [PATCH 13/55] Remove unnecessary space --- packages/code-editor/src/SearchForm/SearchForm.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/code-editor/src/SearchForm/SearchForm.tsx b/packages/code-editor/src/SearchForm/SearchForm.tsx index f73dcd4cce..dd681819d0 100644 --- a/packages/code-editor/src/SearchForm/SearchForm.tsx +++ b/packages/code-editor/src/SearchForm/SearchForm.tsx @@ -79,7 +79,6 @@ export function SearchForm({ view }: SearchFormProps) { const cursor = query.getCursor(view.state.doc); let count = 0; - let result = cursor.next(); while (!result.done) { From 78f21f04ffeb50491919019e3aa9bdbdbd584351 Mon Sep 17 00:00:00 2001 From: Terrence Keane Date: Tue, 30 Sep 2025 14:23:21 -0400 Subject: [PATCH 14/55] Add find selected index rendering --- .../src/SearchForm/SearchForm.styles.ts | 23 ++++- .../code-editor/src/SearchForm/SearchForm.tsx | 94 ++++++++++++++++--- 2 files changed, 103 insertions(+), 14 deletions(-) diff --git a/packages/code-editor/src/SearchForm/SearchForm.styles.ts b/packages/code-editor/src/SearchForm/SearchForm.styles.ts index fa139f2b56..c060af9355 100644 --- a/packages/code-editor/src/SearchForm/SearchForm.styles.ts +++ b/packages/code-editor/src/SearchForm/SearchForm.styles.ts @@ -13,6 +13,7 @@ import { color } from '@leafygreen-ui/tokens'; const CONTAINER_MAX_WIDTH = 500; const SECTION_HEIGHT = 52; const INPUT_WIDTH = 240; +const INPUT_MIN_WIDTH = 120; const getBaseContainerStyles = (theme: Theme) => css` background-color: ${color[theme].background[Variant.Secondary][ @@ -114,9 +115,13 @@ export const getToggleIconStyles = (isOpen: boolean) => export const findInputContainerStyles = css` position: relative; flex: 1 1 ${INPUT_WIDTH}px; - min-width: 100px; + min-width: ${INPUT_MIN_WIDTH}px; max-width: ${INPUT_WIDTH}px; margin-right: ${spacing[100]}px; + + & input { + padding-right: ${spacing[1200]}px; + } `; export const allButtonStyles = css` @@ -127,12 +132,26 @@ export const closeButtonStyles = css` margin-left: auto; `; -export const findInputIconButtonStyles = css` +export const findOptionsContainerStyles = css` position: absolute; right: ${spacing[100]}px; top: ${spacing[100]}px; + display: flex; + align-items: center; `; +// export const findInputIconButtonStyles = css` +// position: absolute; +// right: ${spacing[100]}px; +// top: ${spacing[100]}px; +// `; + +// export const findCountStyles = css` +// position: absolute; +// right: ${spacing[200]}px; +// top: ${spacing[100]}px; +// `; + export const replaceInputContainerStyles = css` position: relative; flex: 1 1 ${INPUT_WIDTH}px; diff --git a/packages/code-editor/src/SearchForm/SearchForm.tsx b/packages/code-editor/src/SearchForm/SearchForm.tsx index dd681819d0..931f279bdd 100644 --- a/packages/code-editor/src/SearchForm/SearchForm.tsx +++ b/packages/code-editor/src/SearchForm/SearchForm.tsx @@ -19,6 +19,7 @@ import Button from '@leafygreen-ui/button'; import IconButton from '@leafygreen-ui/icon-button'; import { useDarkMode } from '@leafygreen-ui/leafygreen-provider'; import TextInput from '@leafygreen-ui/text-input'; +import { Body } from '@leafygreen-ui/typography'; import { Icon } from '../../../icon/src/Icon'; @@ -26,8 +27,8 @@ import { allButtonStyles, closeButtonStyles, findInputContainerStyles, - findInputIconButtonStyles, findInputStyles, + findOptionsContainerStyles, findSectionStyles, getContainerStyles, getReplaceInnerSectionStyles, @@ -44,6 +45,7 @@ export function SearchForm({ view }: SearchFormProps) { const [searchString, setSearchString] = useState(''); const [findCount, setFindCount] = useState(0); const { theme } = useDarkMode(); + const [selectedIndex, setSelectedIndex] = useState(null); const handleToggleButtonClick = useCallback( (_e: MouseEvent) => { @@ -66,6 +68,35 @@ export function SearchForm({ view }: SearchFormProps) { [], ); + const computeSelectedIndex = useCallback(() => { + const query = new SearchQuery({ + search: searchString, + caseSensitive: true, + regexp: false, + wholeWord: false, + replace: '', + }); + + const cursor = query.getCursor(view.state.doc); + const selection = view.state.selection.main; + + let index = 1; + let result = cursor.next(); + + while (!result.done) { + if ( + result.value.from === selection.from && + result.value.to === selection.to + ) { + return index; + } + index++; + result = cursor.next(); + } + + return null; + }, [searchString, view]); + useEffect(() => { const query = new SearchQuery({ search: searchString, @@ -87,16 +118,48 @@ export function SearchForm({ view }: SearchFormProps) { } setFindCount(count); - }, [searchString, view]); + + // Update selected index if current selection matches one of the results + const selection = view.state.selection.main; + const cursor2 = query.getCursor(view.state.doc); + let idx = 1; + let res2 = cursor2.next(); + let currentIndex: number | null = null; + + while (!res2.done) { + if ( + res2.value.from === selection.from && + res2.value.to === selection.to + ) { + currentIndex = idx; + break; + } + idx++; + res2 = cursor2.next(); + } + + setSelectedIndex(currentIndex); + }, [searchString, view, computeSelectedIndex]); const handleFindFormSubmit = useCallback( (e: FormEvent) => { e.preventDefault(); findNext(view); + setSelectedIndex(computeSelectedIndex()); }, - [view], + [view, computeSelectedIndex], ); + const handleNextClick = useCallback(() => { + findNext(view); + setSelectedIndex(computeSelectedIndex()); + }, [view, computeSelectedIndex]); + + const handlePreviousClick = useCallback(() => { + findPrevious(view); + setSelectedIndex(computeSelectedIndex()); + }, [view, computeSelectedIndex]); + return (
- - - +
+ {searchString && ( + + {selectedIndex ?? '?'}/{findCount} + + )} + + + +
findPrevious(view)} + onClick={handlePreviousClick} > findNext(view)} + onClick={handleNextClick} > From 9b8c56755c4df2174cb6cb4013b07becf279958f Mon Sep 17 00:00:00 2001 From: Terrence Keane Date: Wed, 1 Oct 2025 20:11:18 -0400 Subject: [PATCH 15/55] Add replace functionality --- .../code-editor/src/SearchForm/SearchForm.tsx | 186 +++++++++++------- 1 file changed, 111 insertions(+), 75 deletions(-) diff --git a/packages/code-editor/src/SearchForm/SearchForm.tsx b/packages/code-editor/src/SearchForm/SearchForm.tsx index 931f279bdd..234e2acd9a 100644 --- a/packages/code-editor/src/SearchForm/SearchForm.tsx +++ b/packages/code-editor/src/SearchForm/SearchForm.tsx @@ -10,6 +10,8 @@ import { closeSearchPanel, findNext, findPrevious, + replaceAll, + replaceNext, SearchQuery, selectMatches, setSearchQuery, @@ -43,40 +45,24 @@ import { SearchFormProps } from './SearchForm.types'; export function SearchForm({ view }: SearchFormProps) { const [isOpen, setIsOpen] = useState(false); const [searchString, setSearchString] = useState(''); + const [replaceString, setReplaceString] = useState(''); + const [isCaseSensitive, setIsCaseSensitive] = useState(false); + const [isWholeWord, setIsWholeWord] = useState(false); + const [isRegex, setIsRegex] = useState(false); + const [query, setQuery] = useState( + new SearchQuery({ + search: searchString, + caseSensitive: isCaseSensitive, + regexp: isRegex, + wholeWord: isWholeWord, + replace: replaceString, + }), + ); const [findCount, setFindCount] = useState(0); const { theme } = useDarkMode(); const [selectedIndex, setSelectedIndex] = useState(null); - const handleToggleButtonClick = useCallback( - (_e: MouseEvent) => { - setIsOpen(currState => !currState); - }, - [], - ); - - const handleCloseButtonClick = useCallback( - (_e: MouseEvent) => { - closeSearchPanel(view); - }, - [view], - ); - - const handleSearchQueryChange = useCallback( - (_e: ChangeEvent) => { - setSearchString(_e.target.value); - }, - [], - ); - - const computeSelectedIndex = useCallback(() => { - const query = new SearchQuery({ - search: searchString, - caseSensitive: true, - regexp: false, - wholeWord: false, - replace: '', - }); - + const updateSelectedIndex = useCallback(() => { const cursor = query.getCursor(view.state.doc); const selection = view.state.selection.main; @@ -88,26 +74,17 @@ export function SearchForm({ view }: SearchFormProps) { result.value.from === selection.from && result.value.to === selection.to ) { - return index; + setSelectedIndex(index); + return; } index++; result = cursor.next(); } - return null; - }, [searchString, view]); - - useEffect(() => { - const query = new SearchQuery({ - search: searchString, - caseSensitive: true, - regexp: false, - wholeWord: false, - replace: '', - }); - - view.dispatch({ effects: setSearchQuery.of(query) }); + setSelectedIndex(null); + }, [query, view]); + const updateFindCount = useCallback(() => { const cursor = query.getCursor(view.state.doc); let count = 0; let result = cursor.next(); @@ -118,47 +95,97 @@ export function SearchForm({ view }: SearchFormProps) { } setFindCount(count); + }, [query, view]); - // Update selected index if current selection matches one of the results - const selection = view.state.selection.main; - const cursor2 = query.getCursor(view.state.doc); - let idx = 1; - let res2 = cursor2.next(); - let currentIndex: number | null = null; + useEffect(() => { + const newQuery = new SearchQuery({ + search: searchString, + caseSensitive: isCaseSensitive, + regexp: isRegex, + wholeWord: isWholeWord, + replace: replaceString, + }); - while (!res2.done) { - if ( - res2.value.from === selection.from && - res2.value.to === selection.to - ) { - currentIndex = idx; - break; - } - idx++; - res2 = cursor2.next(); - } + setQuery(newQuery); + view.dispatch({ effects: setSearchQuery.of(query) }); + updateFindCount(); + }, [ + replaceString, + searchString, + isCaseSensitive, + isRegex, + isWholeWord, + view, + updateFindCount, + query, + ]); + + const handleToggleButtonClick = useCallback( + (_e: MouseEvent) => { + setIsOpen(currState => !currState); + }, + [], + ); - setSelectedIndex(currentIndex); - }, [searchString, view, computeSelectedIndex]); + const handleCloseButtonClick = useCallback( + (_e: MouseEvent) => { + closeSearchPanel(view); + }, + [view], + ); + + const handleSearchQueryChange = useCallback( + (_e: ChangeEvent) => { + setSearchString(_e.target.value); + }, + [], + ); + + const handleReplaceQueryChange = useCallback( + (_e: ChangeEvent) => { + setReplaceString(_e.target.value); + }, + [], + ); const handleFindFormSubmit = useCallback( (e: FormEvent) => { e.preventDefault(); findNext(view); - setSelectedIndex(computeSelectedIndex()); + updateSelectedIndex(); }, - [view, computeSelectedIndex], + [view, updateSelectedIndex], ); const handleNextClick = useCallback(() => { findNext(view); - setSelectedIndex(computeSelectedIndex()); - }, [view, computeSelectedIndex]); + updateSelectedIndex(); + }, [view, updateSelectedIndex]); const handlePreviousClick = useCallback(() => { findPrevious(view); - setSelectedIndex(computeSelectedIndex()); - }, [view, computeSelectedIndex]); + updateSelectedIndex(); + }, [view, updateSelectedIndex]); + + const handleReplace = useCallback(() => { + replaceNext(view); + updateSelectedIndex(); + updateFindCount(); + }, [view, updateSelectedIndex, updateFindCount]); + + const handleReplaceAll = useCallback(() => { + replaceAll(view); + updateSelectedIndex(); + updateFindCount(); + }, [view, updateSelectedIndex, updateFindCount]); + + const handleReplaceFormSubmit = useCallback( + (e: FormEvent) => { + e.preventDefault(); + handleReplace(); + }, + [handleReplace], + ); return (
- - From 15d794de415496e872089dd6598b9b6a27e57a8c Mon Sep 17 00:00:00 2001 From: Terrence Keane Date: Wed, 1 Oct 2025 20:29:28 -0400 Subject: [PATCH 16/55] Fix keyboard shortcuts --- .../code-editor/src/SearchForm/SearchForm.tsx | 125 ++++++++++-------- 1 file changed, 69 insertions(+), 56 deletions(-) diff --git a/packages/code-editor/src/SearchForm/SearchForm.tsx b/packages/code-editor/src/SearchForm/SearchForm.tsx index 234e2acd9a..7579a77cd8 100644 --- a/packages/code-editor/src/SearchForm/SearchForm.tsx +++ b/packages/code-editor/src/SearchForm/SearchForm.tsx @@ -1,6 +1,6 @@ import React, { ChangeEvent, - FormEvent, + KeyboardEvent, MouseEvent, useCallback, useEffect, @@ -84,18 +84,21 @@ export function SearchForm({ view }: SearchFormProps) { setSelectedIndex(null); }, [query, view]); - const updateFindCount = useCallback(() => { - const cursor = query.getCursor(view.state.doc); - let count = 0; - let result = cursor.next(); + const updateFindCount = useCallback( + (searchQuery: SearchQuery) => { + const cursor = searchQuery.getCursor(view.state.doc); + let count = 0; + let result = cursor.next(); - while (!result.done) { - count++; - result = cursor.next(); - } + while (!result.done) { + count++; + result = cursor.next(); + } - setFindCount(count); - }, [query, view]); + setFindCount(count); + }, + [view], + ); useEffect(() => { const newQuery = new SearchQuery({ @@ -107,8 +110,8 @@ export function SearchForm({ view }: SearchFormProps) { }); setQuery(newQuery); - view.dispatch({ effects: setSearchQuery.of(query) }); - updateFindCount(); + view.dispatch({ effects: setSearchQuery.of(newQuery) }); + updateFindCount(newQuery); }, [ replaceString, searchString, @@ -117,7 +120,6 @@ export function SearchForm({ view }: SearchFormProps) { isWholeWord, view, updateFindCount, - query, ]); const handleToggleButtonClick = useCallback( @@ -148,21 +150,12 @@ export function SearchForm({ view }: SearchFormProps) { [], ); - const handleFindFormSubmit = useCallback( - (e: FormEvent) => { - e.preventDefault(); - findNext(view); - updateSelectedIndex(); - }, - [view, updateSelectedIndex], - ); - - const handleNextClick = useCallback(() => { + const handleFindNext = useCallback(() => { findNext(view); updateSelectedIndex(); }, [view, updateSelectedIndex]); - const handlePreviousClick = useCallback(() => { + const handleFindPrevious = useCallback(() => { findPrevious(view); updateSelectedIndex(); }, [view, updateSelectedIndex]); @@ -170,21 +163,43 @@ export function SearchForm({ view }: SearchFormProps) { const handleReplace = useCallback(() => { replaceNext(view); updateSelectedIndex(); - updateFindCount(); - }, [view, updateSelectedIndex, updateFindCount]); + updateFindCount(query); + }, [view, updateSelectedIndex, updateFindCount, query]); const handleReplaceAll = useCallback(() => { replaceAll(view); updateSelectedIndex(); - updateFindCount(); - }, [view, updateSelectedIndex, updateFindCount]); + updateFindCount(query); + }, [view, updateSelectedIndex, updateFindCount, query]); - const handleReplaceFormSubmit = useCallback( - (e: FormEvent) => { - e.preventDefault(); - handleReplace(); + const handleFindInputKeyDown = useCallback( + (e: KeyboardEvent) => { + if (e.key === 'Enter') { + e.preventDefault(); + if (e.shiftKey) { + handleFindPrevious(); + } else { + handleFindNext(); + } + } else if (e.key === 'Escape') { + e.preventDefault(); + closeSearchPanel(view); + } + }, + [handleFindNext, handleFindPrevious, view], + ); + + const handleReplaceInputKeyDown = useCallback( + (e: KeyboardEvent) => { + if (e.key === 'Enter') { + e.preventDefault(); + handleReplace(); + } else if (e.key === 'Escape') { + e.preventDefault(); + closeSearchPanel(view); + } }, - [handleReplace], + [handleReplace, view], ); return ( @@ -202,17 +217,16 @@ export function SearchForm({ view }: SearchFormProps) {
-
- - +
{searchString && ( @@ -227,14 +241,14 @@ export function SearchForm({ view }: SearchFormProps) { @@ -263,15 +277,14 @@ export function SearchForm({ view }: SearchFormProps) { aria-hidden={!isOpen} >
-
- - + @@ -284,11 +297,15 @@ export function SearchPanel({ view }: SearchPanelProps) { value={replaceString} onChange={handleReplaceQueryChange} onKeyDown={handleReplaceInputKeyDown} + baseFontSize={baseFontSizeProp || baseFontSize} + darkMode={darkMode} /> @@ -296,6 +313,8 @@ export function SearchPanel({ view }: SearchPanelProps) { aria-label="replace all button" className={replaceButtonStyles} onClick={handleReplaceAll} + baseFontSize={baseFontSizeProp || baseFontSize} + darkMode={darkMode} > Replace All diff --git a/packages/code-editor/src/SearchPanel/SearchPanel.types.ts b/packages/code-editor/src/SearchPanel/SearchPanel.types.ts index 6b4651caa3..8111cedbe6 100644 --- a/packages/code-editor/src/SearchPanel/SearchPanel.types.ts +++ b/packages/code-editor/src/SearchPanel/SearchPanel.types.ts @@ -1,5 +1,18 @@ +import { DarkModeProps } from '@leafygreen-ui/lib'; +import { type BaseFontSize } from '@leafygreen-ui/tokens'; + import { CodeMirrorView } from '../CodeEditor'; -export interface SearchPanelProps { +export interface SearchPanelProps extends DarkModeProps { + /** + * Font size of text in the editor. + * + * @default 13 + */ + baseFontSize?: BaseFontSize; + + /** + * The CodeMirror view instance. + */ view: CodeMirrorView; } From 055634a2c129d0a88fa3d874db1296f9daa14a72 Mon Sep 17 00:00:00 2001 From: Terrence Keane Date: Wed, 1 Oct 2025 21:25:29 -0400 Subject: [PATCH 23/55] fix deps --- packages/code-editor/src/CodeEditor/CodeEditor.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/code-editor/src/CodeEditor/CodeEditor.tsx b/packages/code-editor/src/CodeEditor/CodeEditor.tsx index b78fe8d7c2..510fabd11c 100644 --- a/packages/code-editor/src/CodeEditor/CodeEditor.tsx +++ b/packages/code-editor/src/CodeEditor/CodeEditor.tsx @@ -357,6 +357,7 @@ const BaseCodeEditor = forwardRef( forceParsingProp, getContents, enableSearchPanel, + props.darkMode, ]); useImperativeHandle(forwardedRef, () => ({ From c1226bf0eefee722562a25d2e5872d06bd0b6127 Mon Sep 17 00:00:00 2001 From: Terrence Keane Date: Thu, 2 Oct 2025 14:05:15 -0400 Subject: [PATCH 24/55] Fix focus and imports --- packages/code-editor/src/SearchPanel/SearchPanel.tsx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/code-editor/src/SearchPanel/SearchPanel.tsx b/packages/code-editor/src/SearchPanel/SearchPanel.tsx index a64080c92c..7c7b220dea 100644 --- a/packages/code-editor/src/SearchPanel/SearchPanel.tsx +++ b/packages/code-editor/src/SearchPanel/SearchPanel.tsx @@ -17,10 +17,10 @@ import { setSearchQuery, } from '@codemirror/search'; -import Button from '@leafygreen-ui/button'; -import IconButton from '@leafygreen-ui/icon-button'; +import { Button } from '@leafygreen-ui/button'; +import { IconButton } from '@leafygreen-ui/icon-button'; import { useDarkMode } from '@leafygreen-ui/leafygreen-provider'; -import TextInput from '@leafygreen-ui/text-input'; +import { TextInput } from '@leafygreen-ui/text-input'; import { Body, useUpdatedBaseFontSize } from '@leafygreen-ui/typography'; import { Icon } from '../../../icon/src/Icon'; @@ -232,11 +232,13 @@ export function SearchPanel({ onChange={handleSearchQueryChange} onKeyDown={handleFindInputKeyDown} className={findInputStyles} - // eslint-disable-next-line jsx-a11y/no-autofocus - autoFocus value={searchString} baseFontSize={baseFontSizeProp || baseFontSize} darkMode={darkMode} + // eslint-disable-next-line jsx-a11y/no-autofocus + autoFocus + // CodeMirror looks for this attribute to refocus when CMD+F is pressed and the panel is already open + main-field="true" />
{searchString && ( From 7185fd70a6ce51671a7e971ce2bbf6944c034f4a Mon Sep 17 00:00:00 2001 From: Terrence Keane Date: Thu, 2 Oct 2025 14:39:04 -0400 Subject: [PATCH 25/55] prevent panel from overflowing editor --- .../src/CodeEditor/hooks/extensions/useThemeExtension.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/code-editor/src/CodeEditor/hooks/extensions/useThemeExtension.ts b/packages/code-editor/src/CodeEditor/hooks/extensions/useThemeExtension.ts index 843f181c52..9d83e0b2ab 100644 --- a/packages/code-editor/src/CodeEditor/hooks/extensions/useThemeExtension.ts +++ b/packages/code-editor/src/CodeEditor/hooks/extensions/useThemeExtension.ts @@ -76,6 +76,7 @@ export function useThemeExtension({ borderBottomRightRadius: `${borderRadius[300]}px`, borderTopLeftRadius: hasPanel ? 0 : `${borderRadius[300]}px`, borderTopRightRadius: hasPanel ? 0 : `${borderRadius[300]}px`, + overflow: 'hidden', color: color[theme].text[Variant.Primary][InteractionState.Default], }, From 32806fe6e709c5fb96873a980c696d5642b6508c Mon Sep 17 00:00:00 2001 From: Terrence Keane Date: Mon, 6 Oct 2025 11:04:02 -0400 Subject: [PATCH 26/55] add filter menu --- packages/code-editor/package.json | 2 + .../src/SearchPanel/SearchPanel.spec.tsx | 6 -- .../src/SearchPanel/SearchPanel.stories.tsx | 55 ------------------- .../src/SearchPanel/SearchPanel.tsx | 50 ++++++++++++++++- packages/code-editor/tsconfig.json | 6 ++ pnpm-lock.yaml | 6 ++ 6 files changed, 61 insertions(+), 64 deletions(-) delete mode 100644 packages/code-editor/src/SearchPanel/SearchPanel.spec.tsx delete mode 100644 packages/code-editor/src/SearchPanel/SearchPanel.stories.tsx diff --git a/packages/code-editor/package.json b/packages/code-editor/package.json index 5374af1bf6..bad379d390 100644 --- a/packages/code-editor/package.json +++ b/packages/code-editor/package.json @@ -48,10 +48,12 @@ "@leafygreen-ui/a11y": "workspace:^", "@leafygreen-ui/badge": "workspace:^", "@leafygreen-ui/button": "workspace:^", + "@leafygreen-ui/checkbox": "workspace:^", "@leafygreen-ui/emotion": "workspace:^", "@leafygreen-ui/hooks": "workspace:^", "@leafygreen-ui/icon": "workspace:^", "@leafygreen-ui/icon-button": "workspace:^", + "@leafygreen-ui/input-option": "workspace:^", "@leafygreen-ui/leafygreen-provider": "workspace:^", "@leafygreen-ui/lib": "workspace:^", "@leafygreen-ui/menu": "workspace:^", diff --git a/packages/code-editor/src/SearchPanel/SearchPanel.spec.tsx b/packages/code-editor/src/SearchPanel/SearchPanel.spec.tsx deleted file mode 100644 index 92ba4ddf67..0000000000 --- a/packages/code-editor/src/SearchPanel/SearchPanel.spec.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import React from 'react'; -import { render } from '@testing-library/react'; - -import { SearchPanel } from './SearchPanel'; - -describe('SearchPanel', () => {}); diff --git a/packages/code-editor/src/SearchPanel/SearchPanel.stories.tsx b/packages/code-editor/src/SearchPanel/SearchPanel.stories.tsx deleted file mode 100644 index afffba1b04..0000000000 --- a/packages/code-editor/src/SearchPanel/SearchPanel.stories.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import React from 'react'; -import { - storybookArgTypes, - storybookExcludedControlParams, - StoryMetaType, -} from '@lg-tools/storybook-utils'; -import type { StoryFn } from '@storybook/react'; - -import LeafyGreenProvider from '@leafygreen-ui/leafygreen-provider'; -import { BaseFontSize } from '@leafygreen-ui/tokens'; - -import { SearchPanel } from './SearchPanel'; - -const Root = (Story: StoryFn, context: any) => ( - - - -); - -const meta: StoryMetaType = { - title: 'Components/Inputs/CodeEditor/SearchPanel', - component: SearchPanel, - parameters: { - default: 'LiveExample', - controls: { - exclude: [...storybookExcludedControlParams, 'extensions'], - }, - generate: { - combineArgs: { - darkMode: [false, true], - baseFontSize: Object.values(BaseFontSize), - }, - decorator: Root, - }, - }, - decorators: [Root], - args: { - baseFontSize: BaseFontSize.Body1, - darkMode: false, - }, - argTypes: { - darkMode: storybookArgTypes.darkMode, - baseFontSize: storybookArgTypes.updatedBaseFontSize, - }, -}; - -export default meta; - -const Template: StoryFn = args => ; - -export const LiveExample = Template.bind({}); -export const Generated = () => {}; diff --git a/packages/code-editor/src/SearchPanel/SearchPanel.tsx b/packages/code-editor/src/SearchPanel/SearchPanel.tsx index 7c7b220dea..ba33e86e76 100644 --- a/packages/code-editor/src/SearchPanel/SearchPanel.tsx +++ b/packages/code-editor/src/SearchPanel/SearchPanel.tsx @@ -18,8 +18,11 @@ import { } from '@codemirror/search'; import { Button } from '@leafygreen-ui/button'; +import { Checkbox } from '@leafygreen-ui/checkbox'; import { IconButton } from '@leafygreen-ui/icon-button'; +import { InputOption } from '@leafygreen-ui/input-option'; import { useDarkMode } from '@leafygreen-ui/leafygreen-provider'; +import { Menu, MenuVariant } from '@leafygreen-ui/menu'; import { TextInput } from '@leafygreen-ui/text-input'; import { Body, useUpdatedBaseFontSize } from '@leafygreen-ui/typography'; @@ -67,6 +70,15 @@ export function SearchPanel({ const baseFontSize = useUpdatedBaseFontSize(); const [selectedIndex, setSelectedIndex] = useState(null); + const findOptions = { + isCaseSensitive: 'Match case', + isRegex: 'Regexp', + isWholeWord: 'By word', + } as const; + const [highlightedOption, setHighlightedOption] = useState( + null, + ); + const updateSelectedIndex = useCallback(() => { const cursor = query.getCursor(view.state.doc); const selection = view.state.selection.main; @@ -246,9 +258,41 @@ export function SearchPanel({ {selectedIndex ?? '?'}/{findCount} )} - - - + + + + } + renderDarkMenu={false} + variant={MenuVariant.Compact} + > + {Object.entries(findOptions).map(([key, value]) => ( + + { + switch (key) { + case 'isCaseSensitive': + setIsCaseSensitive(!isCaseSensitive); + break; + case 'isRegex': + setIsRegex(!isRegex); + break; + case 'isWholeWord': + setIsWholeWord(!isWholeWord); + break; + } + setHighlightedOption(key); + }} + /> + + ))} +
Date: Mon, 6 Oct 2025 11:06:07 -0400 Subject: [PATCH 27/55] pull out all handler --- packages/code-editor/src/SearchPanel/SearchPanel.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/code-editor/src/SearchPanel/SearchPanel.tsx b/packages/code-editor/src/SearchPanel/SearchPanel.tsx index ba33e86e76..5178144365 100644 --- a/packages/code-editor/src/SearchPanel/SearchPanel.tsx +++ b/packages/code-editor/src/SearchPanel/SearchPanel.tsx @@ -177,6 +177,11 @@ export function SearchPanel({ updateSelectedIndex(); }, [view, updateSelectedIndex]); + const handleFindAll = useCallback(() => { + selectMatches(view); + updateSelectedIndex(); + }, [view, updateSelectedIndex]); + const handleReplace = useCallback(() => { replaceNext(view); updateSelectedIndex(); @@ -312,10 +317,7 @@ export function SearchPanel({ - - - -
+ /** This component is rendered outside of the root so children won't have access to the LGProvider context without this */ +
-
- - + + +
+ +
+ {searchString && ( + + {selectedIndex ?? '?'}/{findCount} + + )} + + + + } + renderDarkMenu={false} + variant={MenuVariant.Compact} + > + {Object.entries(findOptions).map(([key, value]) => ( + + { + switch (key) { + case 'isCaseSensitive': + setIsCaseSensitive(!isCaseSensitive); + break; + case 'isRegex': + setIsRegex(!isRegex); + break; + case 'isWholeWord': + setIsWholeWord(!isWholeWord); + break; + } + setHighlightedOption(key); + }} + /> + + ))} + +
+
+ + + + + + + + + +
+
+
+ + + +
-
+ ); } From ef04229fc4619854e34b7f9ad9d47e00404f2bb5 Mon Sep 17 00:00:00 2001 From: Terrence Keane Date: Thu, 9 Oct 2025 12:37:10 -0400 Subject: [PATCH 44/55] Fix search panel sizing --- .../code-editor/src/CodeEditor/CodeEditor.tsx | 24 +++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/packages/code-editor/src/CodeEditor/CodeEditor.tsx b/packages/code-editor/src/CodeEditor/CodeEditor.tsx index 04780e7fd5..e56eff716a 100644 --- a/packages/code-editor/src/CodeEditor/CodeEditor.tsx +++ b/packages/code-editor/src/CodeEditor/CodeEditor.tsx @@ -273,13 +273,23 @@ const BaseCodeEditor = forwardRef( ? searchModule.search({ createPanel: view => { const dom = document.createElement('div'); - // Your styles remain the same - dom.style.position = 'absolute'; - dom.style.top = '0'; - dom.style.right = '0'; - dom.style.left = '0'; - dom.style.display = 'flex'; - dom.style.justifyContent = 'flex-end'; + + const baseStyles = { + position: 'absolute', + top: '0', + right: '0', + left: '0', + display: 'flex', + justifyContent: 'flex-end', + overflow: 'hidden', + paddingBottom: '5px', // accounts for childs shadow + }; + Object.assign(dom.style, baseStyles); + + if (!panel) { + dom.style.borderTopRightRadius = '12px'; + dom.style.borderTopLeftRadius = '12px'; + } const isReact17 = React.version.startsWith('17'); const searchPanelElement = ( From b01453bc87ee602ad23439f338249a17cf9a5f10 Mon Sep 17 00:00:00 2001 From: Terrence Keane Date: Thu, 9 Oct 2025 12:51:56 -0400 Subject: [PATCH 45/55] Extract search logic into hook --- .../code-editor/src/CodeEditor/CodeEditor.tsx | 87 ++------------- .../extensions/useSearchPanelExtension.tsx | 104 ++++++++++++++++++ 2 files changed, 113 insertions(+), 78 deletions(-) create mode 100644 packages/code-editor/src/CodeEditor/hooks/extensions/useSearchPanelExtension.tsx diff --git a/packages/code-editor/src/CodeEditor/CodeEditor.tsx b/packages/code-editor/src/CodeEditor/CodeEditor.tsx index e56eff716a..4c49a37f15 100644 --- a/packages/code-editor/src/CodeEditor/CodeEditor.tsx +++ b/packages/code-editor/src/CodeEditor/CodeEditor.tsx @@ -20,6 +20,7 @@ import { Panel as CodeEditorPanel } from '../Panel'; import { SearchPanel } from '../SearchPanel'; import { getLgIds } from '../utils'; +import { useSearchPanelExtension } from './hooks/extensions/useSearchPanelExtension'; import { useModules } from './hooks/useModules'; import { getCopyButtonStyles, @@ -247,6 +248,12 @@ const BaseCodeEditor = forwardRef( [getContents, language], ); + const searchPanelExtension = useSearchPanelExtension({ + props, + modules, + hasPanel: !!panel, + }); + useLayoutEffect(() => { const EditorView = modules?.['@codemirror/view']; const commands = modules?.['@codemirror/commands']; @@ -269,84 +276,7 @@ const BaseCodeEditor = forwardRef( commands.history(), - enableSearchPanel && searchModule - ? searchModule.search({ - createPanel: view => { - const dom = document.createElement('div'); - - const baseStyles = { - position: 'absolute', - top: '0', - right: '0', - left: '0', - display: 'flex', - justifyContent: 'flex-end', - overflow: 'hidden', - paddingBottom: '5px', // accounts for childs shadow - }; - Object.assign(dom.style, baseStyles); - - if (!panel) { - dom.style.borderTopRightRadius = '12px'; - dom.style.borderTopLeftRadius = '12px'; - } - - const isReact17 = React.version.startsWith('17'); - const searchPanelElement = ( - - ); - - /** - * This conditional logic is crucial for ensuring the component uses the best rendering - * API for the environment it's in. - * - * While `ReactDOM.render` works in both React 17 and 18, using it in a React 18 - * application is highly discouraged because it forces the app into a legacy, - * synchronous mode. This disables all of React 18's concurrent features, such as - * automatic batching and transitions, sacrificing performance and responsiveness. - * - * By checking the version, we can: - * 1. Use the modern `createRoot` API in React 18 to opt-in to all its benefits. - * 2. Provide a backward-compatible fallback with `ReactDOM.render` for React 17. - * - * We disable the `react/no-deprecated` ESLint rule for the React 17 path because - * we are using these functions intentionally. - */ - if (isReact17) { - // --- React 17 Fallback Path --- - // eslint-disable-next-line react/no-deprecated - ReactDOM.render(searchPanelElement, dom); - - return { - dom, - top: true, - // eslint-disable-next-line react/no-deprecated - unmount: () => ReactDOM.unmountComponentAtNode(dom), - }; - } else { - // --- React 18+ Path --- - let root: any = null; - - (async () => { - const { createRoot } = await import('react-dom/client'); - root = createRoot(dom); - root.render(searchPanelElement); - })(); - - return { - dom, - top: true, - unmount: () => root?.unmount(), - }; - } - }, - }) - : [], + searchPanelExtension, EditorView.EditorView.updateListener.of((update: ViewUpdate) => { if (isControlled && update.docChanged) { @@ -413,6 +343,7 @@ const BaseCodeEditor = forwardRef( props.darkMode, props.baseFontSize, panel, + searchPanelExtension, ]); useImperativeHandle(forwardedRef, () => ({ diff --git a/packages/code-editor/src/CodeEditor/hooks/extensions/useSearchPanelExtension.tsx b/packages/code-editor/src/CodeEditor/hooks/extensions/useSearchPanelExtension.tsx new file mode 100644 index 0000000000..928e5daeb1 --- /dev/null +++ b/packages/code-editor/src/CodeEditor/hooks/extensions/useSearchPanelExtension.tsx @@ -0,0 +1,104 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; + +import { SearchPanel } from '../../../SearchPanel'; +import { CodeEditorProps } from '../../CodeEditor.types'; +import { CodeEditorModules } from '../moduleLoaders.types'; + +export function useSearchPanelExtension({ + props, + modules, + hasPanel, +}: { + props: Partial; + modules: Partial; + hasPanel: boolean; +}) { + const { enableSearchPanel } = props; + const searchModule = modules?.['@codemirror/search']; + + if (!enableSearchPanel || !searchModule) { + return []; + } + + const searchPanelExtension = searchModule.search({ + createPanel: view => { + const dom = document.createElement('div'); + + const baseStyles = { + position: 'absolute', + top: '0', + right: '0', + left: '0', + display: 'flex', + justifyContent: 'flex-end', + overflow: 'hidden', + paddingBottom: '5px', // accounts for childs shadow + }; + + Object.assign(dom.style, baseStyles); + + if (!hasPanel) { + dom.style.borderTopRightRadius = '12px'; + dom.style.borderTopLeftRadius = '12px'; + } + + const isReact17 = React.version.startsWith('17'); + + const searchPanelElement = ( + + ); + + /** + * This conditional logic is crucial for ensuring the component uses the best rendering + * API for the environment it's in. + * + * While `ReactDOM.render` works in both React 17 and 18, using it in a React 18 + * application is highly discouraged because it forces the app into a legacy, + * synchronous mode. This disables all of React 18's concurrent features, such as + * automatic batching and transitions, sacrificing performance and responsiveness. + * + * By checking the version, we can: + * 1. Use the modern `createRoot` API in React 18 to opt-in to all its benefits. + * 2. Provide a backward-compatible fallback with `ReactDOM.render` for React 17. + * + * We disable the `react/no-deprecated` ESLint rule for the React 17 path because + * we are using these functions intentionally. + */ + if (isReact17) { + // --- React 17 Fallback Path --- + // eslint-disable-next-line react/no-deprecated + ReactDOM.render(searchPanelElement, dom); + + return { + dom, + top: true, + // eslint-disable-next-line react/no-deprecated + unmount: () => ReactDOM.unmountComponentAtNode(dom), + }; + } else { + // --- React 18+ Path --- + let root: any = null; + + (async () => { + const { createRoot } = await import('react-dom/client'); + root = createRoot(dom); + root.render(searchPanelElement); + })(); + + return { + dom, + top: true, + unmount: () => root?.unmount(), + }; + } + }, + }); + + return searchPanelExtension; +} From 02b3ea21d0abe3484fb3a97425fae04d17b54eac Mon Sep 17 00:00:00 2001 From: Terrence Keane Date: Thu, 9 Oct 2025 15:12:36 -0400 Subject: [PATCH 46/55] Remove unused variables --- packages/code-editor/src/CodeEditor/CodeEditor.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/code-editor/src/CodeEditor/CodeEditor.tsx b/packages/code-editor/src/CodeEditor/CodeEditor.tsx index 4c49a37f15..089c01d543 100644 --- a/packages/code-editor/src/CodeEditor/CodeEditor.tsx +++ b/packages/code-editor/src/CodeEditor/CodeEditor.tsx @@ -6,7 +6,6 @@ import React, { useRef, useState, } from 'react'; -import ReactDOM from 'react-dom'; import { type EditorView, type ViewUpdate } from '@codemirror/view'; import { useDarkMode } from '@leafygreen-ui/leafygreen-provider'; @@ -17,7 +16,6 @@ import { CodeEditorContextMenu } from '../CodeEditorContextMenu'; import { CodeEditorCopyButton } from '../CodeEditorCopyButton'; import { CopyButtonVariant } from '../CodeEditorCopyButton/CodeEditorCopyButton.types'; import { Panel as CodeEditorPanel } from '../Panel'; -import { SearchPanel } from '../SearchPanel'; import { getLgIds } from '../utils'; import { useSearchPanelExtension } from './hooks/extensions/useSearchPanelExtension'; From 2609d7d1a010ab6d92a1f06c24474ef01067a309 Mon Sep 17 00:00:00 2001 From: Terrence Keane Date: Thu, 9 Oct 2025 15:13:07 -0400 Subject: [PATCH 47/55] Reapply "Update search match background to match Code highlight" This reverts commit ec510e519eeaa1fc9c9da6c2882407dd98bd628c. --- packages/code-editor/src/CodeEditor/CodeEditor.types.ts | 2 ++ .../src/CodeEditor/hooks/extensions/useThemeExtension.ts | 7 +++++++ 2 files changed, 9 insertions(+) diff --git a/packages/code-editor/src/CodeEditor/CodeEditor.types.ts b/packages/code-editor/src/CodeEditor/CodeEditor.types.ts index 863228cb3e..4e5a471732 100644 --- a/packages/code-editor/src/CodeEditor/CodeEditor.types.ts +++ b/packages/code-editor/src/CodeEditor/CodeEditor.types.ts @@ -97,6 +97,8 @@ export const CodeEditorSelectors = { LineWrapping: '.cm-lineWrapping', SearchInput: 'input[type="text"], .cm-textfield, input[placeholder*="search" i]', + SearchMatch: '.cm-searchMatch', + SearchMatchSelected: '.cm-searchMatch-selected', SearchPanel: '.cm-panel', SearchPanelContainer: '.cm-panels', SearchPanelContainerTop: '.cm-panels-top', diff --git a/packages/code-editor/src/CodeEditor/hooks/extensions/useThemeExtension.ts b/packages/code-editor/src/CodeEditor/hooks/extensions/useThemeExtension.ts index 843f181c52..4ac122f653 100644 --- a/packages/code-editor/src/CodeEditor/hooks/extensions/useThemeExtension.ts +++ b/packages/code-editor/src/CodeEditor/hooks/extensions/useThemeExtension.ts @@ -151,6 +151,13 @@ export function useThemeExtension({ [CodeEditorSelectors.SearchPanelContainerTop]: { border: 'none', }, + + [`${CodeEditorSelectors.SearchMatch}:not(${CodeEditorSelectors.SearchMatchSelected})`]: + { + backgroundColor: 'rgba(254, 247, 219, 1)', + borderTop: '1px solid rgba(255, 236, 158, 1)', + borderBottom: '1px solid rgba(255, 236, 158, 1)', + }, }, { dark: theme === Theme.Dark }, ); From 729b4653a901413c73a2439bc15bd55dfbfff82b Mon Sep 17 00:00:00 2001 From: Terrence Keane Date: Thu, 9 Oct 2025 15:37:44 -0400 Subject: [PATCH 48/55] Possible color pallette for find --- .../hooks/extensions/useThemeExtension.ts | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/packages/code-editor/src/CodeEditor/hooks/extensions/useThemeExtension.ts b/packages/code-editor/src/CodeEditor/hooks/extensions/useThemeExtension.ts index 4ac122f653..d1054f979e 100644 --- a/packages/code-editor/src/CodeEditor/hooks/extensions/useThemeExtension.ts +++ b/packages/code-editor/src/CodeEditor/hooks/extensions/useThemeExtension.ts @@ -2,6 +2,7 @@ import { type EditorView } from '@codemirror/view'; import { useDarkMode } from '@leafygreen-ui/leafygreen-provider'; import { Theme } from '@leafygreen-ui/lib'; +import { palette } from '@leafygreen-ui/palette'; import { borderRadius, color, @@ -152,11 +153,23 @@ export function useThemeExtension({ border: 'none', }, - [`${CodeEditorSelectors.SearchMatch}:not(${CodeEditorSelectors.SearchMatchSelected})`]: + [`${CodeEditorSelectors.SearchMatch}:not(${CodeEditorSelectors.SearchMatchSelected}), + ${CodeEditorSelectors.SearchMatch}:not(${CodeEditorSelectors.SearchMatchSelected}) > *`]: { - backgroundColor: 'rgba(254, 247, 219, 1)', - borderTop: '1px solid rgba(255, 236, 158, 1)', - borderBottom: '1px solid rgba(255, 236, 158, 1)', + backgroundColor: palette.yellow.light2, + color: + color[Theme.Light].text[Variant.Primary][ + InteractionState.Default + ], + }, + + [`${CodeEditorSelectors.SearchMatchSelected}, ${CodeEditorSelectors.SearchMatchSelected} > *`]: + { + backgroundColor: `${palette.yellow.base}`, + color: + color[Theme.Light].text[Variant.Primary][ + InteractionState.Default + ], }, }, { dark: theme === Theme.Dark }, From 23ca739dcd394be8ef51eaf61ece86f8fe7418d4 Mon Sep 17 00:00:00 2001 From: Terrence Keane Date: Thu, 9 Oct 2025 15:43:33 -0400 Subject: [PATCH 49/55] Remove 'all' option --- .../src/SearchPanel/SearchPanel.styles.ts | 4 ---- .../code-editor/src/SearchPanel/SearchPanel.tsx | 14 -------------- 2 files changed, 18 deletions(-) diff --git a/packages/code-editor/src/SearchPanel/SearchPanel.styles.ts b/packages/code-editor/src/SearchPanel/SearchPanel.styles.ts index 7778969e33..04e4ccb3ad 100644 --- a/packages/code-editor/src/SearchPanel/SearchPanel.styles.ts +++ b/packages/code-editor/src/SearchPanel/SearchPanel.styles.ts @@ -137,10 +137,6 @@ export const findInputContainerStyles = css` } `; -export const allButtonStyles = css` - margin-left: ${spacing[100]}px; -`; - export const closeButtonStyles = css` margin-left: auto; `; diff --git a/packages/code-editor/src/SearchPanel/SearchPanel.tsx b/packages/code-editor/src/SearchPanel/SearchPanel.tsx index 923647e180..c656156b16 100644 --- a/packages/code-editor/src/SearchPanel/SearchPanel.tsx +++ b/packages/code-editor/src/SearchPanel/SearchPanel.tsx @@ -14,7 +14,6 @@ import { replaceAll, replaceNext, SearchQuery, - selectMatches, setSearchQuery, } from '@codemirror/search'; @@ -31,7 +30,6 @@ import { TextInput } from '@leafygreen-ui/text-input'; import { Body, useUpdatedBaseFontSize } from '@leafygreen-ui/typography'; import { - allButtonStyles, closeButtonStyles, findInputContainerStyles, findInputStyles, @@ -222,11 +220,6 @@ export function SearchPanel({ updateSelectedIndex(); }, [view, updateSelectedIndex]); - const handleFindAll = useCallback(() => { - selectMatches(view); - updateSelectedIndex(); - }, [view, updateSelectedIndex]); - const handleReplace = useCallback(() => { replaceNext(view); updateSelectedIndex(); @@ -360,13 +353,6 @@ export function SearchPanel({ > - Date: Fri, 10 Oct 2025 10:13:44 -0400 Subject: [PATCH 50/55] Fix build? --- .../hooks/extensions/useSearchPanelExtension.tsx | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/packages/code-editor/src/CodeEditor/hooks/extensions/useSearchPanelExtension.tsx b/packages/code-editor/src/CodeEditor/hooks/extensions/useSearchPanelExtension.tsx index 928e5daeb1..89521d28f4 100644 --- a/packages/code-editor/src/CodeEditor/hooks/extensions/useSearchPanelExtension.tsx +++ b/packages/code-editor/src/CodeEditor/hooks/extensions/useSearchPanelExtension.tsx @@ -83,18 +83,14 @@ export function useSearchPanelExtension({ }; } else { // --- React 18+ Path --- - let root: any = null; - - (async () => { - const { createRoot } = await import('react-dom/client'); - root = createRoot(dom); - root.render(searchPanelElement); - })(); + const { createRoot } = require('react-dom/client'); + const root = createRoot(dom); + root.render(searchPanelElement); return { dom, top: true, - unmount: () => root?.unmount(), + unmount: () => root.unmount(), }; } }, From fc4efa12fa9640f2020c4d239d98b2fda4f51d58 Mon Sep 17 00:00:00 2001 From: Terrence Keane Date: Fri, 10 Oct 2025 10:44:20 -0400 Subject: [PATCH 51/55] Add require v import comment --- .../hooks/extensions/useSearchPanelExtension.tsx | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/code-editor/src/CodeEditor/hooks/extensions/useSearchPanelExtension.tsx b/packages/code-editor/src/CodeEditor/hooks/extensions/useSearchPanelExtension.tsx index 89521d28f4..63ea957845 100644 --- a/packages/code-editor/src/CodeEditor/hooks/extensions/useSearchPanelExtension.tsx +++ b/packages/code-editor/src/CodeEditor/hooks/extensions/useSearchPanelExtension.tsx @@ -83,6 +83,16 @@ export function useSearchPanelExtension({ }; } else { // --- React 18+ Path --- + + /** + * We use require() instead of import() here to avoid TypeScript build errors + * in React 17 environments. While import() would be more idiomatic for dynamic + * imports, TypeScript attempts to resolve 'react-dom/client' types at compile + * time even for dynamic imports, causing the build to fail when this module + * doesn't exist (React 17). The require() call bypasses this strict type + * checking, allowing the code to build for both React 17 and 18, while still + * loading the module correctly at runtime when React 18 is detected. + */ const { createRoot } = require('react-dom/client'); const root = createRoot(dom); root.render(searchPanelElement); From 8f3cab1324e65525ee128c5c7a185b50650c9c0f Mon Sep 17 00:00:00 2001 From: Terrence Keane Date: Fri, 10 Oct 2025 11:43:57 -0400 Subject: [PATCH 52/55] Fix hook logic --- packages/code-editor/src/CodeEditor/CodeEditor.tsx | 6 +++++- .../hooks/extensions/useSearchPanelExtension.tsx | 9 +++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/code-editor/src/CodeEditor/CodeEditor.tsx b/packages/code-editor/src/CodeEditor/CodeEditor.tsx index 089c01d543..7def42d257 100644 --- a/packages/code-editor/src/CodeEditor/CodeEditor.tsx +++ b/packages/code-editor/src/CodeEditor/CodeEditor.tsx @@ -247,7 +247,11 @@ const BaseCodeEditor = forwardRef( ); const searchPanelExtension = useSearchPanelExtension({ - props, + props: { + ...props, + darkMode, + baseFontSize, + }, modules, hasPanel: !!panel, }); diff --git a/packages/code-editor/src/CodeEditor/hooks/extensions/useSearchPanelExtension.tsx b/packages/code-editor/src/CodeEditor/hooks/extensions/useSearchPanelExtension.tsx index 63ea957845..fb34a9e8b2 100644 --- a/packages/code-editor/src/CodeEditor/hooks/extensions/useSearchPanelExtension.tsx +++ b/packages/code-editor/src/CodeEditor/hooks/extensions/useSearchPanelExtension.tsx @@ -14,10 +14,11 @@ export function useSearchPanelExtension({ modules: Partial; hasPanel: boolean; }) { - const { enableSearchPanel } = props; + const { enableSearchPanel, darkMode, baseFontSize } = props; const searchModule = modules?.['@codemirror/search']; - if (!enableSearchPanel || !searchModule) { + /** If enableSearchPanel is undefined, we default to true */ + if (enableSearchPanel === false || !searchModule) { return []; } @@ -48,8 +49,8 @@ export function useSearchPanelExtension({ const searchPanelElement = ( ); From a504d7912c3be2335423d3d9c426e884b66415a4 Mon Sep 17 00:00:00 2001 From: Terrence Keane Date: Mon, 20 Oct 2025 17:12:32 -0400 Subject: [PATCH 53/55] refactor(SearchPanel): update selected index handling to use searchQuery parameter for improved accuracy --- .../src/SearchPanel/SearchPanel.tsx | 51 ++++++++++--------- 1 file changed, 28 insertions(+), 23 deletions(-) diff --git a/packages/code-editor/src/SearchPanel/SearchPanel.tsx b/packages/code-editor/src/SearchPanel/SearchPanel.tsx index c656156b16..5b88e4b71b 100644 --- a/packages/code-editor/src/SearchPanel/SearchPanel.tsx +++ b/packages/code-editor/src/SearchPanel/SearchPanel.tsx @@ -84,25 +84,28 @@ export function SearchPanel({ const isInitialRender = useRef(true); const inputRef = useRef(null); - const updateSelectedIndex = useCallback(() => { - const cursor = query.getCursor(view.state.doc); - const selection = view.state.selection.main; - let index = 1; - let result = cursor.next(); + const updateSelectedIndex = useCallback( + (searchQuery: SearchQuery) => { + const cursor = searchQuery.getCursor(view.state.doc); + const selection = view.state.selection.main; + let index = 1; + let result = cursor.next(); - while (!result.done) { - if ( - result.value.from === selection.from && - result.value.to === selection.to - ) { - setSelectedIndex(index); - return; + while (!result.done) { + if ( + result.value.from === selection.from && + result.value.to === selection.to + ) { + setSelectedIndex(index); + return; + } + index++; + result = cursor.next(); } - index++; - result = cursor.next(); - } - setSelectedIndex(null); - }, [query, view]); + setSelectedIndex(null); + }, + [view], + ); const updateFindCount = useCallback( (searchQuery: SearchQuery) => { @@ -155,6 +158,7 @@ export function SearchPanel({ } updateFindCount(newQuery); + updateSelectedIndex(newQuery); }, [ replaceString, searchString, @@ -163,6 +167,7 @@ export function SearchPanel({ isWholeWord, view, updateFindCount, + updateSelectedIndex, ]); /** @@ -212,23 +217,23 @@ export function SearchPanel({ const handleFindNext = useCallback(() => { findNext(view); - updateSelectedIndex(); - }, [view, updateSelectedIndex]); + updateSelectedIndex(query); + }, [view, updateSelectedIndex, query]); const handleFindPrevious = useCallback(() => { findPrevious(view); - updateSelectedIndex(); - }, [view, updateSelectedIndex]); + updateSelectedIndex(query); + }, [view, updateSelectedIndex, query]); const handleReplace = useCallback(() => { replaceNext(view); - updateSelectedIndex(); + updateSelectedIndex(query); updateFindCount(query); }, [view, updateSelectedIndex, updateFindCount, query]); const handleReplaceAll = useCallback(() => { replaceAll(view); - updateSelectedIndex(); + updateSelectedIndex(query); updateFindCount(query); }, [view, updateSelectedIndex, updateFindCount, query]); From 3c6aa517df359bbca644b9e994ebab24edb0f4c2 Mon Sep 17 00:00:00 2001 From: Terrence Keane Date: Mon, 20 Oct 2025 17:13:04 -0400 Subject: [PATCH 54/55] fix(SearchPanel): remove TypeScript error suppression for ref prop in TextInput component --- packages/code-editor/src/SearchPanel/SearchPanel.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/code-editor/src/SearchPanel/SearchPanel.tsx b/packages/code-editor/src/SearchPanel/SearchPanel.tsx index 5b88e4b71b..521826678f 100644 --- a/packages/code-editor/src/SearchPanel/SearchPanel.tsx +++ b/packages/code-editor/src/SearchPanel/SearchPanel.tsx @@ -298,7 +298,6 @@ export function SearchPanel({ value={searchString} // CodeMirror looks for this attribute to refocus when CMD+F is pressed and the panel is already open main-field="true" - // @ts-expect-error - The TextInput component forwards refs, but the types do not explicitly include the `ref` prop. ref={inputRef} />
From 3ed5b3344cc035f0089a7771ec53a9919dda87f1 Mon Sep 17 00:00:00 2001 From: Terrence Keane Date: Mon, 20 Oct 2025 17:15:39 -0400 Subject: [PATCH 55/55] fix(SearchPanel): adjust positioning and add right border to enhance layout --- .../src/CodeEditor/hooks/extensions/useSearchPanelExtension.tsx | 2 +- packages/code-editor/src/SearchPanel/SearchPanel.styles.ts | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/code-editor/src/CodeEditor/hooks/extensions/useSearchPanelExtension.tsx b/packages/code-editor/src/CodeEditor/hooks/extensions/useSearchPanelExtension.tsx index fb34a9e8b2..66866950d2 100644 --- a/packages/code-editor/src/CodeEditor/hooks/extensions/useSearchPanelExtension.tsx +++ b/packages/code-editor/src/CodeEditor/hooks/extensions/useSearchPanelExtension.tsx @@ -29,7 +29,7 @@ export function useSearchPanelExtension({ const baseStyles = { position: 'absolute', top: '0', - right: '0', + right: '-1px', left: '0', display: 'flex', justifyContent: 'flex-end', diff --git a/packages/code-editor/src/SearchPanel/SearchPanel.styles.ts b/packages/code-editor/src/SearchPanel/SearchPanel.styles.ts index 04e4ccb3ad..5c0fae874a 100644 --- a/packages/code-editor/src/SearchPanel/SearchPanel.styles.ts +++ b/packages/code-editor/src/SearchPanel/SearchPanel.styles.ts @@ -24,6 +24,8 @@ const getBaseContainerStyles = ( InteractionState.Default ]}; font-size: ${baseFontSize}px; + border-right: 1px solid + ${color[theme].border[Variant.Secondary][InteractionState.Default]}; border-bottom-left-radius: ${borderRadius[150]}px; border-bottom-right-radius: ${borderRadius[150]}px; width: 100%;