From b7ac4248dc50d30c44d5a6658208a12415f34bab Mon Sep 17 00:00:00 2001 From: saxumcordis Date: Thu, 9 Oct 2025 18:07:37 +0300 Subject: [PATCH 1/4] feat: add replace to markup search --- src/i18n/search/en.json | 7 ++- src/i18n/search/ru.json | 7 ++- src/markup/codemirror/search-plugin/plugin.ts | 39 ++++++++---- .../search-plugin/view/ReplaceIcons.tsx | 15 +++++ .../search-plugin/view/SearchPopup.tsx | 59 +++++++++++++++---- src/types/svg.d.ts | 6 ++ 6 files changed, 105 insertions(+), 28 deletions(-) create mode 100644 src/markup/codemirror/search-plugin/view/ReplaceIcons.tsx create mode 100644 src/types/svg.d.ts diff --git a/src/i18n/search/en.json b/src/i18n/search/en.json index 670d3b3e1..3d03107b0 100644 --- a/src/i18n/search/en.json +++ b/src/i18n/search/en.json @@ -1,5 +1,8 @@ { "label_case-sensitive": "Case sensitive", "label_whole-word": "Whole word", - "title": "Search in code" -} + "title": "Search in code", + "action_replace": "Replace", + "action_replace_all": "Replace all", + "replace_placeholder": "Replacement text" +} \ No newline at end of file diff --git a/src/i18n/search/ru.json b/src/i18n/search/ru.json index 6e19ad55a..2d4a5d001 100644 --- a/src/i18n/search/ru.json +++ b/src/i18n/search/ru.json @@ -1,5 +1,8 @@ { "label_case-sensitive": "С учетом регистра", "label_whole-word": "Слово целиком", - "title": "Найти в коде" -} + "title": "Найти в коде", + "action_replace": "Заменить", + "action_replace_all": "Заменить всё", + "replace_placeholder": "Текст замены" +} \ No newline at end of file diff --git a/src/markup/codemirror/search-plugin/plugin.ts b/src/markup/codemirror/search-plugin/plugin.ts index 114743955..972c711dd 100644 --- a/src/markup/codemirror/search-plugin/plugin.ts +++ b/src/markup/codemirror/search-plugin/plugin.ts @@ -4,6 +4,8 @@ import { findNext, findPrevious, getSearchQuery, + replaceAll, + replaceNext, search, searchKeymap, searchPanelOpen, @@ -17,14 +19,14 @@ import { keymap, } from '@codemirror/view'; -import type {MarkdownEditorMode} from '../../../bundle'; -import type {EventMap} from '../../../bundle/Editor'; -import type {RendererItem} from '../../../extensions'; -import {debounce} from '../../../lodash'; -import type {Receiver} from '../../../utils'; -import {ReactRendererFacet} from '../react-facet'; +import type { MarkdownEditorMode } from '../../../bundle'; +import type { EventMap } from '../../../bundle/Editor'; +import type { RendererItem } from '../../../extensions'; +import { debounce } from '../../../lodash'; +import type { Receiver } from '../../../utils'; +import { ReactRendererFacet } from '../react-facet'; -import {renderSearchPopup} from './view/SearchPopup'; +import { renderSearchPopup } from './view/SearchPopup'; type SearchQueryConfig = ConstructorParameters[0]; @@ -48,6 +50,7 @@ export const SearchPanelPlugin = (params: SearchPanelPluginParams) => search: '', caseSensitive: false, wholeWord: false, + replace: '', }; receiver: Receiver | undefined; @@ -64,6 +67,8 @@ export const SearchPanelPlugin = (params: SearchPanelPluginParams) => this.handleChange = this.handleChange.bind(this); this.handleSearchNext = this.handleSearchNext.bind(this); this.handleSearchPrev = this.handleSearchPrev.bind(this); + this.handleReplaceNext = this.handleReplaceNext.bind(this); + this.handleReplaceAll = this.handleReplaceAll.bind(this); this.handleSearchConfigChange = this.handleSearchConfigChange.bind(this); this.handleEditorModeChange = this.handleEditorModeChange.bind(this); @@ -91,6 +96,8 @@ export const SearchPanelPlugin = (params: SearchPanelPluginParams) => onClose: this.handleClose, onSearchNext: this.handleSearchNext, onSearchPrev: this.handleSearchPrev, + onReplaceNext: this.handleReplaceNext, + onReplaceAll: this.handleReplaceAll, onConfigChange: this.handleSearchConfigChange, }), ); @@ -115,21 +122,21 @@ export const SearchPanelPlugin = (params: SearchPanelPluginParams) => ...this.searchConfig, }); - this.view.dispatch({effects: setSearchQuery.of(searchQuery)}); + this.view.dispatch({ effects: setSearchQuery.of(searchQuery) }); } - handleEditorModeChange({mode}: {mode: MarkdownEditorMode}) { + handleEditorModeChange({ mode }: { mode: MarkdownEditorMode }) { if (mode === 'wysiwyg') { closeSearchPanel(this.view); } } handleChange(search: string) { - this.setViewSearchWithDelay({search}); + this.setViewSearchWithDelay({ search }); } handleClose() { - this.setViewSearch({search: ''}); + this.setViewSearch({ search: '' }); closeSearchPanel(this.view); } @@ -144,6 +151,16 @@ export const SearchPanelPlugin = (params: SearchPanelPluginParams) => handleSearchConfigChange(config: Partial) { this.setViewSearch(config); } + + handleReplaceNext(query: string, replacement: string) { + this.setViewSearch({ search: query, replace: replacement }); + replaceNext(this.view); + } + + handleReplaceAll(query: string, replacement: string) { + this.setViewSearch({ search: query, replace: replacement }); + replaceAll(this.view); + } }, { provide: () => [ diff --git a/src/markup/codemirror/search-plugin/view/ReplaceIcons.tsx b/src/markup/codemirror/search-plugin/view/ReplaceIcons.tsx new file mode 100644 index 000000000..2d9d13477 --- /dev/null +++ b/src/markup/codemirror/search-plugin/view/ReplaceIcons.tsx @@ -0,0 +1,15 @@ +import React from 'react'; + +export const ReplaceIcon: React.FC> = (props) => ( + + + +); + +export const ReplaceAllIcon: React.FC> = (props) => ( + + + +); + + diff --git a/src/markup/codemirror/search-plugin/view/SearchPopup.tsx b/src/markup/codemirror/search-plugin/view/SearchPopup.tsx index 5b9fa81d8..d475ebe70 100644 --- a/src/markup/codemirror/search-plugin/view/SearchPopup.tsx +++ b/src/markup/codemirror/search-plugin/view/SearchPopup.tsx @@ -1,7 +1,7 @@ -import {useRef, useState} from 'react'; +import { useRef, useState } from 'react'; -import type {SearchQuery} from '@codemirror/search'; -import {ChevronDown, ChevronUp, Xmark} from '@gravity-ui/icons'; +import type { SearchQuery } from '@codemirror/search'; +import { ChevronDown, ChevronUp, Xmark } from '@gravity-ui/icons'; import { Button, Card, @@ -13,11 +13,12 @@ import { sp, } from '@gravity-ui/uikit'; -import {cn} from '../../../../classname'; -import {i18n} from '../../../../i18n/search'; -import {enterKeyHandler} from '../../../../utils/handlers'; +import { cn } from '../../../../classname'; +import { i18n } from '../../../../i18n/search'; +import { enterKeyHandler } from '../../../../utils/handlers'; import './SearchPopup.scss'; +import { ReplaceIcon, ReplaceAllIcon } from './ReplaceIcons'; type SearchInitial = Pick; type SearchConfig = Pick; @@ -29,12 +30,14 @@ interface SearchCardProps { onClose?: (query: string) => void; onSearchPrev?: (query: string) => void; onSearchNext?: (query: string) => void; + onReplaceNext?: (query: string, replacement: string) => void; + onReplaceAll?: (query: string, replacement: string) => void; onConfigChange?: (config: SearchConfig) => void; } const b = cn('search-card'); -const noop = () => {}; +const noop = () => { }; const inverse = (val: boolean) => !val; export const SearchCard: React.FC = ({ @@ -43,11 +46,14 @@ export const SearchCard: React.FC = ({ onClose = noop, onSearchPrev = noop, onSearchNext = noop, + onReplaceNext = noop, + onReplaceAll = noop, onConfigChange = noop, }) => { const [query, setQuery] = useState(initial.search); const [isCaseSensitive, setIsCaseSensitive] = useState(initial.caseSensitive); const [isWholeWord, setIsWholeWord] = useState(initial.wholeWord); + const [replacement, setReplacement] = useState(''); const textInputRef = useRef(null); const setInputFocus = () => { @@ -75,6 +81,16 @@ export const SearchCard: React.FC = ({ setInputFocus(); }; + const handleReplace = () => { + onReplaceNext(query, replacement); + setInputFocus(); + }; + + const handleReplaceAll = () => { + onReplaceAll(query, replacement); + setInputFocus(); + }; + const handleIsCaseSensitive = () => { onConfigChange({ caseSensitive: !isCaseSensitive, @@ -105,7 +121,7 @@ export const SearchCard: React.FC = ({ = ({ value={query} endContent={ <> - - } /> + + + + + } + /> {i18n('label_case-sensitive')} @@ -143,7 +176,7 @@ export interface SearchPopupProps extends SearchCardProps { onClose: () => void; } -export const SearchPopup: React.FC = ({open, anchor, ...props}) => { +export const SearchPopup: React.FC = ({ open, anchor, ...props }) => { return ( { anchor: HTMLElement | null; } -export function renderSearchPopup({anchor, ...props}: SearchPopupWithRefProps) { +export function renderSearchPopup({ anchor, ...props }: SearchPopupWithRefProps) { return <>{anchor && }; } diff --git a/src/types/svg.d.ts b/src/types/svg.d.ts new file mode 100644 index 000000000..418529ea6 --- /dev/null +++ b/src/types/svg.d.ts @@ -0,0 +1,6 @@ +declare module '*.svg' { + const ReactComponent: React.ComponentType>; + export default ReactComponent; +} + + From 9eb5f642c3d34fc6a663c02aeee77b0f08415b9b Mon Sep 17 00:00:00 2001 From: saxumcordis Date: Thu, 9 Oct 2025 18:13:19 +0300 Subject: [PATCH 2/4] fix prettier --- src/markup/codemirror/search-plugin/plugin.ts | 26 +++++------ .../search-plugin/view/ReplaceIcons.tsx | 38 ++++++++++++---- .../search-plugin/view/SearchPopup.tsx | 43 ++++++++++++------- src/types/svg.d.ts | 6 --- 4 files changed, 70 insertions(+), 43 deletions(-) delete mode 100644 src/types/svg.d.ts diff --git a/src/markup/codemirror/search-plugin/plugin.ts b/src/markup/codemirror/search-plugin/plugin.ts index 972c711dd..86dd5b775 100644 --- a/src/markup/codemirror/search-plugin/plugin.ts +++ b/src/markup/codemirror/search-plugin/plugin.ts @@ -19,14 +19,14 @@ import { keymap, } from '@codemirror/view'; -import type { MarkdownEditorMode } from '../../../bundle'; -import type { EventMap } from '../../../bundle/Editor'; -import type { RendererItem } from '../../../extensions'; -import { debounce } from '../../../lodash'; -import type { Receiver } from '../../../utils'; -import { ReactRendererFacet } from '../react-facet'; +import type {MarkdownEditorMode} from '../../../bundle'; +import type {EventMap} from '../../../bundle/Editor'; +import type {RendererItem} from '../../../extensions'; +import {debounce} from '../../../lodash'; +import type {Receiver} from '../../../utils'; +import {ReactRendererFacet} from '../react-facet'; -import { renderSearchPopup } from './view/SearchPopup'; +import {renderSearchPopup} from './view/SearchPopup'; type SearchQueryConfig = ConstructorParameters[0]; @@ -122,21 +122,21 @@ export const SearchPanelPlugin = (params: SearchPanelPluginParams) => ...this.searchConfig, }); - this.view.dispatch({ effects: setSearchQuery.of(searchQuery) }); + this.view.dispatch({effects: setSearchQuery.of(searchQuery)}); } - handleEditorModeChange({ mode }: { mode: MarkdownEditorMode }) { + handleEditorModeChange({mode}: {mode: MarkdownEditorMode}) { if (mode === 'wysiwyg') { closeSearchPanel(this.view); } } handleChange(search: string) { - this.setViewSearchWithDelay({ search }); + this.setViewSearchWithDelay({search}); } handleClose() { - this.setViewSearch({ search: '' }); + this.setViewSearch({search: ''}); closeSearchPanel(this.view); } @@ -153,12 +153,12 @@ export const SearchPanelPlugin = (params: SearchPanelPluginParams) => } handleReplaceNext(query: string, replacement: string) { - this.setViewSearch({ search: query, replace: replacement }); + this.setViewSearch({search: query, replace: replacement}); replaceNext(this.view); } handleReplaceAll(query: string, replacement: string) { - this.setViewSearch({ search: query, replace: replacement }); + this.setViewSearch({search: query, replace: replacement}); replaceAll(this.view); } }, diff --git a/src/markup/codemirror/search-plugin/view/ReplaceIcons.tsx b/src/markup/codemirror/search-plugin/view/ReplaceIcons.tsx index 2d9d13477..de8202838 100644 --- a/src/markup/codemirror/search-plugin/view/ReplaceIcons.tsx +++ b/src/markup/codemirror/search-plugin/view/ReplaceIcons.tsx @@ -1,15 +1,35 @@ -import React from 'react'; +import type React from 'react'; export const ReplaceIcon: React.FC> = (props) => ( - - - + + + ); export const ReplaceAllIcon: React.FC> = (props) => ( - - - + + + ); - - diff --git a/src/markup/codemirror/search-plugin/view/SearchPopup.tsx b/src/markup/codemirror/search-plugin/view/SearchPopup.tsx index d475ebe70..a58fd131e 100644 --- a/src/markup/codemirror/search-plugin/view/SearchPopup.tsx +++ b/src/markup/codemirror/search-plugin/view/SearchPopup.tsx @@ -1,7 +1,7 @@ -import { useRef, useState } from 'react'; +import {useRef, useState} from 'react'; -import type { SearchQuery } from '@codemirror/search'; -import { ChevronDown, ChevronUp, Xmark } from '@gravity-ui/icons'; +import type {SearchQuery} from '@codemirror/search'; +import {ChevronDown, ChevronUp, Xmark} from '@gravity-ui/icons'; import { Button, Card, @@ -13,12 +13,13 @@ import { sp, } from '@gravity-ui/uikit'; -import { cn } from '../../../../classname'; -import { i18n } from '../../../../i18n/search'; -import { enterKeyHandler } from '../../../../utils/handlers'; +import {cn} from '../../../../classname'; +import {i18n} from '../../../../i18n/search'; +import {enterKeyHandler} from '../../../../utils/handlers'; + +import {ReplaceAllIcon, ReplaceIcon} from './ReplaceIcons'; import './SearchPopup.scss'; -import { ReplaceIcon, ReplaceAllIcon } from './ReplaceIcons'; type SearchInitial = Pick; type SearchConfig = Pick; @@ -37,7 +38,7 @@ interface SearchCardProps { const b = cn('search-card'); -const noop = () => { }; +const noop = () => {}; const inverse = (val: boolean) => !val; export const SearchCard: React.FC = ({ @@ -121,7 +122,7 @@ export const SearchCard: React.FC = ({ = ({ /> - - @@ -159,7 +172,7 @@ export const SearchCard: React.FC = ({ size="m" onUpdate={handleIsCaseSensitive} checked={isCaseSensitive} - className={sp({ mr: 4 })} + className={sp({mr: 4})} > {i18n('label_case-sensitive')} @@ -176,7 +189,7 @@ export interface SearchPopupProps extends SearchCardProps { onClose: () => void; } -export const SearchPopup: React.FC = ({ open, anchor, ...props }) => { +export const SearchPopup: React.FC = ({open, anchor, ...props}) => { return ( { anchor: HTMLElement | null; } -export function renderSearchPopup({ anchor, ...props }: SearchPopupWithRefProps) { +export function renderSearchPopup({anchor, ...props}: SearchPopupWithRefProps) { return <>{anchor && }; } diff --git a/src/types/svg.d.ts b/src/types/svg.d.ts deleted file mode 100644 index 418529ea6..000000000 --- a/src/types/svg.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -declare module '*.svg' { - const ReactComponent: React.ComponentType>; - export default ReactComponent; -} - - From aaa8e93c4aef02867069841c5abd5b84610d9ef2 Mon Sep 17 00:00:00 2001 From: saxumcordis Date: Tue, 14 Oct 2025 13:16:11 +0300 Subject: [PATCH 3/4] fix width --- src/markup/codemirror/search-plugin/view/SearchPopup.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/src/markup/codemirror/search-plugin/view/SearchPopup.scss b/src/markup/codemirror/search-plugin/view/SearchPopup.scss index 7a7a30dd2..c28586707 100644 --- a/src/markup/codemirror/search-plugin/view/SearchPopup.scss +++ b/src/markup/codemirror/search-plugin/view/SearchPopup.scss @@ -1,5 +1,6 @@ .g-md-search-card { padding: var(--g-spacing-2) var(--g-spacing-2) var(--g-spacing-3) var(--g-spacing-4); + width: 450px; &__header { display: flex; From e9ccbfb1e1bcc7b3a80d556f079773fad1033dbd Mon Sep 17 00:00:00 2001 From: saxumcordis Date: Tue, 14 Oct 2025 15:18:26 +0300 Subject: [PATCH 4/4] fix lint --- src/markup/codemirror/search-plugin/view/SearchPopup.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/markup/codemirror/search-plugin/view/SearchPopup.scss b/src/markup/codemirror/search-plugin/view/SearchPopup.scss index c28586707..743728f39 100644 --- a/src/markup/codemirror/search-plugin/view/SearchPopup.scss +++ b/src/markup/codemirror/search-plugin/view/SearchPopup.scss @@ -1,6 +1,6 @@ .g-md-search-card { - padding: var(--g-spacing-2) var(--g-spacing-2) var(--g-spacing-3) var(--g-spacing-4); width: 450px; + padding: var(--g-spacing-2) var(--g-spacing-2) var(--g-spacing-3) var(--g-spacing-4); &__header { display: flex;