Skip to content

Commit 5c36b3d

Browse files
saxumcordissaxumcordis
andauthored
feat: add replace func to markup search (#860)
Co-authored-by: saxumcordis <[email protected]>
1 parent 776de5c commit 5c36b3d

File tree

6 files changed

+111
-6
lines changed

6 files changed

+111
-6
lines changed

src/i18n/search/en.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
{
22
"label_case-sensitive": "Case sensitive",
33
"label_whole-word": "Whole word",
4-
"title": "Search in code"
5-
}
4+
"title": "Search in code",
5+
"action_replace": "Replace",
6+
"action_replace_all": "Replace all",
7+
"replace_placeholder": "Replacement text"
8+
}

src/i18n/search/ru.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
{
22
"label_case-sensitive": "С учетом регистра",
33
"label_whole-word": "Слово целиком",
4-
"title": "Найти в коде"
5-
}
4+
"title": "Найти в коде",
5+
"action_replace": "Заменить",
6+
"action_replace_all": "Заменить всё",
7+
"replace_placeholder": "Текст замены"
8+
}

src/markup/codemirror/search-plugin/plugin.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import {
44
findNext,
55
findPrevious,
66
getSearchQuery,
7+
replaceAll,
8+
replaceNext,
79
search,
810
searchKeymap,
911
searchPanelOpen,
@@ -48,6 +50,7 @@ export const SearchPanelPlugin = (params: SearchPanelPluginParams) =>
4850
search: '',
4951
caseSensitive: false,
5052
wholeWord: false,
53+
replace: '',
5154
};
5255
receiver: Receiver<EventMap> | undefined;
5356

@@ -64,6 +67,8 @@ export const SearchPanelPlugin = (params: SearchPanelPluginParams) =>
6467
this.handleChange = this.handleChange.bind(this);
6568
this.handleSearchNext = this.handleSearchNext.bind(this);
6669
this.handleSearchPrev = this.handleSearchPrev.bind(this);
70+
this.handleReplaceNext = this.handleReplaceNext.bind(this);
71+
this.handleReplaceAll = this.handleReplaceAll.bind(this);
6772
this.handleSearchConfigChange = this.handleSearchConfigChange.bind(this);
6873
this.handleEditorModeChange = this.handleEditorModeChange.bind(this);
6974

@@ -91,6 +96,8 @@ export const SearchPanelPlugin = (params: SearchPanelPluginParams) =>
9196
onClose: this.handleClose,
9297
onSearchNext: this.handleSearchNext,
9398
onSearchPrev: this.handleSearchPrev,
99+
onReplaceNext: this.handleReplaceNext,
100+
onReplaceAll: this.handleReplaceAll,
94101
onConfigChange: this.handleSearchConfigChange,
95102
}),
96103
);
@@ -144,6 +151,16 @@ export const SearchPanelPlugin = (params: SearchPanelPluginParams) =>
144151
handleSearchConfigChange(config: Partial<SearchQueryConfig>) {
145152
this.setViewSearch(config);
146153
}
154+
155+
handleReplaceNext(query: string, replacement: string) {
156+
this.setViewSearch({search: query, replace: replacement});
157+
replaceNext(this.view);
158+
}
159+
160+
handleReplaceAll(query: string, replacement: string) {
161+
this.setViewSearch({search: query, replace: replacement});
162+
replaceAll(this.view);
163+
}
147164
},
148165
{
149166
provide: () => [
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import type React from 'react';
2+
3+
export const ReplaceIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => (
4+
<svg
5+
viewBox="0 0 16 16"
6+
xmlns="http://www.w3.org/2000/svg"
7+
fill="currentColor"
8+
width={14}
9+
height={14}
10+
{...props}
11+
>
12+
<path
13+
fillRule="evenodd"
14+
clipRule="evenodd"
15+
d="M3.221 3.739l2.261 2.269L7.7 3.784l-.7-.7-1.012 1.007-.008-1.6a.523.523 0 0 1 .5-.526H8V1H6.48A1.482 1.482 0 0 0 5 2.489V4.1L3.927 3.033l-.706.706zm6.67 1.794h.01c.183.311.451.467.806.467.393 0 .706-.168.94-.503.236-.335.353-.78.353-1.333 0-.511-.1-.913-.301-1.207-.201-.295-.488-.442-.86-.442-.405 0-.718.194-.938.581h-.01V1H9v4.919h.89v-.386zm-.015-1.061v-.34c0-.248.058-.448.175-.601a.54.54 0 0 1 .445-.23.49.49 0 0 1 .436.233c.104.154.155.368.155.643 0 .33-.056.587-.169.768a.524.524 0 0 1-.47.27.495.495 0 0 1-.411-.211.853.853 0 0 1-.16-.532zM9 12.769c-.256.154-.625.231-1.108.231-.563 0-1.02-.178-1.369-.533-.349-.355-.523-.813-.523-1.374 0-.648.186-1.158.56-1.53.374-.376.875-.563 1.5-.563.433 0 .746.06.94.179v.998a1.26 1.26 0 0 0-.792-.276c-.325 0-.583.1-.774.298-.19.196-.283.468-.283.816 0 .338.09.603.272.797.182.191.431.287.749.287.282 0 .558-.092.828-.276v.946zM4 7L3 8v6l1 1h7l1-1V8l-1-1H4zm0 1h7v6H4V8z"
16+
/>
17+
</svg>
18+
);
19+
20+
export const ReplaceAllIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => (
21+
<svg
22+
viewBox="0 0 16 16"
23+
xmlns="http://www.w3.org/2000/svg"
24+
fill="currentColor"
25+
width={14}
26+
height={14}
27+
{...props}
28+
>
29+
<path
30+
fillRule="evenodd"
31+
clipRule="evenodd"
32+
d="M11.6 2.677c.147-.31.356-.465.626-.465.248 0 .44.118.573.353.134.236.201.557.201.966 0 .443-.078.798-.235 1.067-.156.268-.365.402-.627.402-.237 0-.416-.125-.537-.374h-.008v.31H11V1h.593v1.677h.008zm-.016 1.1a.78.78 0 0 0 .107.426c.071.113.163.169.274.169.136 0 .24-.072.314-.216.075-.145.113-.35.113-.615 0-.22-.035-.39-.104-.514-.067-.124-.164-.187-.29-.187-.12 0-.219.062-.297.185a.886.886 0 0 0-.117.48v.272zM4.12 7.695L2 5.568l.662-.662 1.006 1v-1.51A1.39 1.39 0 0 1 5.055 3H7.4v.905H5.055a.49.49 0 0 0-.468.493l.007 1.5.949-.944.656.656-2.08 2.085zM9.356 4.93H10V3.22C10 2.408 9.685 2 9.056 2c-.135 0-.285.024-.45.073a1.444 1.444 0 0 0-.388.167v.665c.237-.203.487-.304.75-.304.261 0 .392.156.392.469l-.6.103c-.506.086-.76.406-.76.961 0 .263.061.473.183.631A.61.61 0 0 0 8.69 5c.29 0 .509-.16.657-.48h.009v.41zm.004-1.355v.193a.75.75 0 0 1-.12.436.368.368 0 0 1-.313.17.276.276 0 0 1-.22-.095.38.38 0 0 1-.08-.248c0-.222.11-.351.332-.389l.4-.067zM7 12.93h-.644v-.41h-.009c-.148.32-.367.48-.657.48a.61.61 0 0 1-.507-.235c-.122-.158-.183-.368-.183-.63 0-.556.254-.876.76-.962l.6-.103c0-.313-.13-.47-.392-.47-.263 0-.513.102-.75.305v-.665c.095-.063.224-.119.388-.167.165-.049.315-.073.45-.073.63 0 .944.407.944 1.22v1.71zm-.64-1.162v-.193l-.4.068c-.222.037-.333.166-.333.388 0 .1.027.183.08.248a.276.276 0 0 0 .22.095.368.368 0 0 0 .312-.17c.08-.116.12-.26.12-.436zM9.262 13c.321 0 .568-.058.738-.173v-.71a.9.9 0 0 1-.552.207.619.619 0 0 1-.5-.215c-.12-.145-.181-.345-.181-.598 0-.26.063-.464.189-.612a.644.644 0 0 1 .516-.223c.194 0 .37.069.528.207v-.749c-.129-.09-.338-.134-.626-.134-.417 0-.751.14-1.001.422-.249.28-.373.662-.373 1.148 0 .42.116.764.349 1.03.232.267.537.4.913.4zM2 9l1-1h9l1 1v5l-1 1H3l-1-1V9zm1 0v5h9V9H3zm3-2l1-1h7l1 1v5l-1 1V7H6z"
33+
/>
34+
</svg>
35+
);

src/markup/codemirror/search-plugin/view/SearchPopup.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
.g-md-search-card {
2+
width: 450px;
23
padding: var(--g-spacing-2) var(--g-spacing-2) var(--g-spacing-3) var(--g-spacing-4);
34

45
&__header {

src/markup/codemirror/search-plugin/view/SearchPopup.tsx

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import {cn} from '../../../../classname';
1717
import {i18n} from '../../../../i18n/search';
1818
import {enterKeyHandler} from '../../../../utils/handlers';
1919

20+
import {ReplaceAllIcon, ReplaceIcon} from './ReplaceIcons';
21+
2022
import './SearchPopup.scss';
2123

2224
type SearchInitial = Pick<SearchQuery, 'search' | 'caseSensitive' | 'wholeWord'>;
@@ -29,6 +31,8 @@ interface SearchCardProps {
2931
onClose?: (query: string) => void;
3032
onSearchPrev?: (query: string) => void;
3133
onSearchNext?: (query: string) => void;
34+
onReplaceNext?: (query: string, replacement: string) => void;
35+
onReplaceAll?: (query: string, replacement: string) => void;
3236
onConfigChange?: (config: SearchConfig) => void;
3337
}
3438

@@ -43,11 +47,14 @@ export const SearchCard: React.FC<SearchCardProps> = ({
4347
onClose = noop,
4448
onSearchPrev = noop,
4549
onSearchNext = noop,
50+
onReplaceNext = noop,
51+
onReplaceAll = noop,
4652
onConfigChange = noop,
4753
}) => {
4854
const [query, setQuery] = useState<string>(initial.search);
4955
const [isCaseSensitive, setIsCaseSensitive] = useState<boolean>(initial.caseSensitive);
5056
const [isWholeWord, setIsWholeWord] = useState<boolean>(initial.wholeWord);
57+
const [replacement, setReplacement] = useState<string>('');
5158
const textInputRef = useRef<HTMLInputElement>(null);
5259

5360
const setInputFocus = () => {
@@ -75,6 +82,16 @@ export const SearchCard: React.FC<SearchCardProps> = ({
7582
setInputFocus();
7683
};
7784

85+
const handleReplace = () => {
86+
onReplaceNext(query, replacement);
87+
setInputFocus();
88+
};
89+
90+
const handleReplaceAll = () => {
91+
onReplaceAll(query, replacement);
92+
setInputFocus();
93+
};
94+
7895
const handleIsCaseSensitive = () => {
7996
onConfigChange({
8097
caseSensitive: !isCaseSensitive,
@@ -113,15 +130,44 @@ export const SearchCard: React.FC<SearchCardProps> = ({
113130
value={query}
114131
endContent={
115132
<>
116-
<Button onClick={handlePrev}>
133+
<Button onClick={handlePrev} pin="round-brick">
117134
<Icon data={ChevronUp} size={12} />
118135
</Button>
119-
<Button onClick={handleNext}>
136+
<Button onClick={handleNext} pin="brick-round">
120137
<Icon data={ChevronDown} size={12} />
121138
</Button>
122139
</>
123140
}
124141
/>
142+
<TextInput
143+
placeholder={i18n('replace_placeholder')}
144+
className={sp({mb: 2})}
145+
size="s"
146+
onUpdate={setReplacement}
147+
value={replacement}
148+
endContent={
149+
<>
150+
<Button
151+
size="s"
152+
onClick={handleReplace}
153+
pin="round-brick"
154+
disabled={!query}
155+
title={i18n('action_replace')}
156+
>
157+
<ReplaceIcon width={12} height={12} />
158+
</Button>
159+
<Button
160+
size="s"
161+
onClick={handleReplaceAll}
162+
pin="brick-round"
163+
disabled={!query}
164+
title={i18n('action_replace_all')}
165+
>
166+
<ReplaceAllIcon width={12} height={12} />
167+
</Button>
168+
</>
169+
}
170+
/>
125171
<Checkbox
126172
size="m"
127173
onUpdate={handleIsCaseSensitive}

0 commit comments

Comments
 (0)