From 279ef2c2d89a7d9ee8cde766d8f81a014713f7d2 Mon Sep 17 00:00:00 2001 From: MatthewChenShow <158122452+MatthewChenShow@users.noreply.github.com> Date: Fri, 7 Jun 2024 17:55:46 +0800 Subject: [PATCH] feat: support search replace text * fix: Esc to close searchFile input * feat: add 'Escape' to close search text and click background to close search file * fix: search text input css * feat: add search text debounce * fix: search result use result text * Update index.tsx * feat: support add extra libs * feat: once add default extra lib * feat: add exmaple * feat: change readme * feat: add file action menu * fix: action menu css * fix: unset useFileMenu param * chore: readme * feat: add react css auto complete * fix: react d.ts url * feat: replace/replaceAll search text * fix: search text refresh * fix: selected row error for replacing single row * fix: formate searchfile/searchtext code * fix: readme add editor image * chore: readme update * fix: replace text escapeRegExp * fix: prettier use singleQuote * fix: prettier use singleQuote * fix: UI Fix * fix: replace icon & readme --- README.md | 4 +- src/components/icons/replace.tsx | 16 ++ src/components/searchfile/index.tsx | 13 +- .../searchfile/search-file-body.tsx | 58 +++++-- src/components/searchtext/index.tsx | 160 +++++++++++++++--- .../searchtext/search-file-title.tsx | 7 +- src/components/searchtext/search-input.tsx | 88 +++++++--- src/components/searchtext/search-result.tsx | 104 ++++++++---- src/components/searchtext/search-text.less | 94 +++++++++- src/multi/Editor.tsx | 14 +- 10 files changed, 445 insertions(+), 113 deletions(-) create mode 100644 src/components/icons/replace.tsx diff --git a/README.md b/README.md index 485ade0..51350f4 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,6 @@ # monaco-base-ide +![img](https://p6.music.126.net/obj/wo3DlcOGw6DClTvDisK1/36505433932/638f/06f4/a958/4d82bdeff7c5c5afa437028f68ed1b65.png) +![img2](https://p5.music.126.net/obj/wo3DlcOGw6DClTvDisK1/36505435944/01f8/2388/f3e9/bafe10aedf6a954f4ce09bf02c725748.png) ## 如何使用 @@ -142,7 +144,7 @@ export default SingleIDE; #### 搜索文件 快捷键:command/ctrl + p -#### 搜索文本 +#### 搜索 & 替换文本 快捷键:shift + command/ctrl + f #### 类型提示声明 diff --git a/src/components/icons/replace.tsx b/src/components/icons/replace.tsx new file mode 100644 index 0000000..8aa1bca --- /dev/null +++ b/src/components/icons/replace.tsx @@ -0,0 +1,16 @@ +const Replace = (props: any) => ( + + + + + ); + export default Replace; \ No newline at end of file diff --git a/src/components/searchfile/index.tsx b/src/components/searchfile/index.tsx index a37ca1d..faea903 100644 --- a/src/components/searchfile/index.tsx +++ b/src/components/searchfile/index.tsx @@ -10,7 +10,9 @@ interface SearchFileProps { const SearchFile: React.FC = (props) => { const [searchResults, setSearchResults] = useState([]); - const filenames = useState(Array.isArray(props.list) ? [...props.list] : [])[0]; + const filenames = useState( + Array.isArray(props.list) ? [...props.list] : [] + )[0]; const onSelectFile = props.onSelectFile; const onClose = props.onClose; @@ -30,8 +32,11 @@ const SearchFile: React.FC = (props) => { }; return ( -
-
e.stopPropagation()}> +
+
e.stopPropagation()} + > = (props) => { ); }; -export default SearchFile; \ No newline at end of file +export default SearchFile; diff --git a/src/components/searchfile/search-file-body.tsx b/src/components/searchfile/search-file-body.tsx index 6ff1a7f..d3589d3 100644 --- a/src/components/searchfile/search-file-body.tsx +++ b/src/components/searchfile/search-file-body.tsx @@ -1,4 +1,10 @@ -import React, { useState, useRef, useEffect, ChangeEvent, KeyboardEvent } from 'react'; +import React, { + useState, + useRef, + useEffect, + ChangeEvent, + KeyboardEvent, +} from 'react'; import Icon from '@components/icons'; interface SearchModalProps { @@ -7,7 +13,11 @@ interface SearchModalProps { onExecute: (result: string) => void; } -const SearchModal: React.FC = ({onSearch, searchResults, onExecute }) => { +const SearchModal: React.FC = ({ + onSearch, + searchResults, + onExecute, +}) => { const [searchQuery, setSearchQuery] = useState(''); const [selectedItem, setSelectedItem] = useState(0); const inputRef = useRef(null); @@ -19,9 +29,14 @@ const SearchModal: React.FC = ({onSearch, searchResults, onExe useEffect(() => { if (searchResults.length > 0 && modalRef.current) { - const selectedItemElement = modalRef.current.childNodes[selectedItem] as HTMLElement; + const selectedItemElement = modalRef.current.childNodes[ + selectedItem + ] as HTMLElement; if (selectedItemElement) { - selectedItemElement.scrollIntoView({ behavior: 'smooth', block: 'center' }); + selectedItemElement.scrollIntoView({ + behavior: 'smooth', + block: 'center', + }); } } }, [selectedItem, searchResults]); @@ -34,7 +49,9 @@ const SearchModal: React.FC = ({onSearch, searchResults, onExe setSelectedItem((prev) => (prev + 1) % searchResults.length); } else if (e.key === 'ArrowUp') { e.preventDefault(); - setSelectedItem((prev) => (prev - 1 + searchResults.length) % searchResults.length); + setSelectedItem( + (prev) => (prev - 1 + searchResults.length) % searchResults.length + ); } else if (e.key === 'Enter') { e.preventDefault(); onExecute(searchResults[selectedItem]); @@ -63,7 +80,7 @@ const SearchModal: React.FC = ({onSearch, searchResults, onExe
e.stopPropagation()}> = ({onSearch, searchResults, onExe placeholder="Search for files..." /> {searchResults.length > 0 && ( -
    +
      {searchResults.map((result, index) => (
    • onClickLine(index)} > - + {result}
    • ))} @@ -97,4 +129,4 @@ const SearchModal: React.FC = ({onSearch, searchResults, onExe ); }; -export default SearchModal; \ No newline at end of file +export default SearchModal; diff --git a/src/components/searchtext/index.tsx b/src/components/searchtext/index.tsx index 440c4c0..8e7d3a4 100644 --- a/src/components/searchtext/index.tsx +++ b/src/components/searchtext/index.tsx @@ -1,13 +1,16 @@ -import React, { useState, useEffect, useCallback, useRef } from "react"; -import SearchInput from "./search-input"; -import SearchResult from "./search-result"; -import "./search-text.less"; +import React, { useState, useEffect, useCallback, useRef } from 'react'; +import SearchInput from './search-input'; +import SearchResult from './search-result'; +import './search-text.less'; +import Modal from '@components/modal'; interface SearchAndReplaceProps { onSelectedLine: (title: string, line: number) => void; listFiles: Record; style?: React.CSSProperties; onClose: React.Dispatch>; + onReplace: (listFiles: Record) => void; + rootEl: React.MutableRefObject; } interface SelectedRow { @@ -22,9 +25,12 @@ const SearchAndReplace: React.FC = ({ listFiles, style, onClose, + onReplace, + rootEl, }) => { - const [searchText, setSearchText] = useState(""); - const [resultText, setResultText] = useState(""); + const [searchText, setSearchText] = useState(''); + const [replaceText, setReplaceText] = useState(''); + const [resultText, setResultText] = useState(''); const [searchResults, setSearchResults] = useState([]); const [unExpandedTitles, setUnExpandedTitles] = useState< Record @@ -36,6 +42,7 @@ const SearchAndReplace: React.FC = ({ const [allSelectResults, setAllSelectResults] = useState< { titleIndex: number; rowIndex: number }[] >([]); + const [expand, setExpand] = useState(false); const innerRef = useRef(null); @@ -60,7 +67,7 @@ const SearchAndReplace: React.FC = ({ const lsearchResults: SearchResultType = []; for (const [key, value] of Object.entries(listFiles)) { - const matches = value.split("\n"); + const matches = value.split('\n'); if (matches) { const matchingSubstrings = []; for (let i = 0; i < matches.length; i++) { @@ -79,6 +86,81 @@ const SearchAndReplace: React.FC = ({ setSearchResults(lsearchResults); }, [resultText, listFiles]); + const replaceAll = useCallback(() => { + let length = 0; + searchResults.forEach((item) => { + for (const [key, values] of Object.entries(item)) { + length += values.length; + } + }); + + Modal.confirm({ + target: rootEl.current, + okText: '确定', + onOk: (ok: () => void) => { + for (const [key, value] of Object.entries(listFiles)) { + handleReplaceFile(key, value); + } + onReplace(listFiles); + ok(); + }, + title: '确定要全部替换吗?', + content: () => ( +
      +
      + 涉及 {searchResults.length} 文件,共 {length} 行 +
      +
      + ), + }); + }, [replaceText, listFiles, searchResults]); + + const handleReplaceLine = useCallback( + (fileName: string, line: number) => { + const matches = listFiles[fileName].split('\n'); + if (matches && matches.length > line - 1) { + matches[line - 1] = handleReplaceCode(matches[line - 1], replaceText); + listFiles[fileName] = matches.join('\n'); + } + onReplace(listFiles); + }, + [replaceText, listFiles] + ); + + const handleReplaceCode = useCallback( + (code: string, replaceText: string) => { + const escapeRegExp = (text: string) => { + return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&'); + }; + let regex = new RegExp(escapeRegExp(searchText), 'gi'); + return code.replace(regex, replaceText); + }, + [searchText] + ); + + const handleReplaceFile = useCallback( + (fileName: string, code: string) => { + const matches = code.split('\n'); + searchResults.forEach((result) => { + for (const [resultKey, resultValues] of Object.entries(result)) { + if (resultKey === fileName) { + resultValues.forEach((resultValue) => { + if (matches && matches.length > resultValue.line - 1) { + matches[resultValue.line - 1] = handleReplaceCode( + resultValue.code, + replaceText + ); + } + }); + } + } + }); + const newValue = matches.join('\n'); + listFiles[fileName] = newValue; + }, + [replaceText, listFiles] + ); + const smoothSelectedResults = useCallback(() => { const selectedResults: { titleIndex: number; rowIndex: number }[] = []; searchResults.forEach((result, titleIndex) => { @@ -120,16 +202,11 @@ const SearchAndReplace: React.FC = ({ ); const handleKeyDown = useCallback( - (event: { - metaKey: any; - shiftKey: any; - key: string; - preventDefault: () => void; - }) => { - if (event.key === "ArrowDown") { + (event: KeyboardEvent) => { + if (event.key === 'ArrowDown') { event.preventDefault(); setSelectedRow((pre) => nextRow(pre.titleIndex, pre.rowIndex)); - } else if (event.key === "ArrowUp") { + } else if (event.key === 'ArrowUp') { event.preventDefault(); setSelectedRow((pre) => preRow(pre.titleIndex, pre.rowIndex)); } @@ -148,9 +225,9 @@ const SearchAndReplace: React.FC = ({ useEffect(() => { const current = innerRef?.current as unknown as HTMLElement; if (current) { - current.addEventListener("keydown", handleKeyDown); + current.addEventListener('keydown', handleKeyDown); return () => { - current.removeEventListener("keydown", handleKeyDown); + current.removeEventListener('keydown', handleKeyDown); }; } }, [innerRef, handleKeyDown]); @@ -166,7 +243,9 @@ const SearchAndReplace: React.FC = ({ const title = keys[0]; const entry = searchResults[selectedRow.titleIndex][title][selectedRow.rowIndex]; - onSelectedLine && onSelectedLine(title, entry.line); + if (entry) { + onSelectedLine && onSelectedLine(title, entry?.line); + } } } }, [selectedRow]); @@ -179,32 +258,55 @@ const SearchAndReplace: React.FC = ({ setSelectedRow({ titleIndex: -1, rowIndex: -1 }); }; - const handleRowSelection = ( - titleIndex: any, - title: any, - rowIndex: any, - row: any - ) => { + const handleRowSelection = (titleIndex: any, rowIndex: any) => { setSelectedRow({ titleIndex, rowIndex }); }; + const replaceRowSelection = ( + event: React.MouseEvent + ) => { + if ( + selectedRow.titleIndex >= 0 && + searchResults && + searchResults.length > selectedRow.titleIndex + ) { + event.preventDefault(); + event.stopPropagation(); + const keys = Object.keys(searchResults[selectedRow.titleIndex]); + if (keys && keys.length > 0) { + const key = keys[0]; + const entry = + searchResults[selectedRow.titleIndex][key][selectedRow.rowIndex]; + if (entry) { + setSelectedRow({ titleIndex: -1, rowIndex: -1 }); + handleReplaceLine(key, entry.line); + } + } + } + }; + return (
      = ({ selectedRow={selectedRow} handleRowSelection={handleRowSelection} toggleExpand={toggleExpand} + replaceRowSelection={replaceRowSelection} + canReplace={expand} />
      ); diff --git a/src/components/searchtext/search-file-title.tsx b/src/components/searchtext/search-file-title.tsx index 20cf349..7938c97 100644 --- a/src/components/searchtext/search-file-title.tsx +++ b/src/components/searchtext/search-file-title.tsx @@ -14,6 +14,7 @@ const SearchFileTitle: React.FC = (props) => { const renderTitle = (titleText: string) => { const fileName = titleText.split('/').pop(); + const fileDir = titleText.split('/').slice(0, -1).join('/'); let fileType; if (fileName && fileName.indexOf('.') !== -1) { fileType = `file_type_${fileName.split('.').slice(-1)}`; @@ -31,8 +32,8 @@ const SearchFileTitle: React.FC = (props) => { marginRight: '5px', }} /> - {fileName} - {titleText} + {fileName} + {fileDir}
); }; @@ -49,4 +50,4 @@ const SearchFileTitle: React.FC = (props) => { ); }; -export default SearchFileTitle; \ No newline at end of file +export default SearchFileTitle; diff --git a/src/components/searchtext/search-input.tsx b/src/components/searchtext/search-input.tsx index 5108675..483a534 100644 --- a/src/components/searchtext/search-input.tsx +++ b/src/components/searchtext/search-input.tsx @@ -1,39 +1,87 @@ -import React, { useEffect, useRef } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import Close from '@components/icons/close'; +import Replace from '@components/icons/replace'; +import Arrow from '@components/icons/arrow'; interface SearchInputProps { searchText: string; setSearchText: React.Dispatch>; + replaceText: string; + setReplaceText: React.Dispatch>; onClose: React.Dispatch>; + onReplace: () => void; + expand: boolean; + setExpand: React.Dispatch>; } -const SearchInput: React.FC = ({ searchText, setSearchText, onClose }) => { +const SearchInput: React.FC = ({ + searchText, + setSearchText, + replaceText, + setReplaceText, + onClose, + onReplace, + expand, + setExpand, +}) => { const inputRef = useRef(null); useEffect(() => { inputRef.current?.focus(); }, [inputRef]); - + return ( -
- setSearchText(e.target.value)} - placeholder="搜索(上下键切换)" - /> -
onClose(false)} - className="music-monaco-editor--close"> - +
setExpand((pre) => !pre)} + > + +
+
+
+ setSearchText(e.target.value)} + placeholder="搜索(上下键切换)" /> +
onClose(false)} + className="music-monaco-editor-close" + > + +
+ + {expand && ( +
+ setReplaceText(e.target.value)} + placeholder="替换" + /> +
+ +
+
+ )} +
); }; -export default SearchInput; \ No newline at end of file +export default SearchInput; diff --git a/src/components/searchtext/search-result.tsx b/src/components/searchtext/search-result.tsx index aa776c5..813b530 100644 --- a/src/components/searchtext/search-result.tsx +++ b/src/components/searchtext/search-result.tsx @@ -1,60 +1,96 @@ import React from 'react'; -import SearchFileTitle from './search-file-title' +import SearchFileTitle from './search-file-title'; +import Replace from '@components/icons/replace'; interface SearchResultProps { searchResults: Array<{ [key: string]: Array<{ code: string }> }>; unExpandedTitles: Record; searchText: string; selectedRow: { titleIndex: number; rowIndex: number }; - handleRowSelection: (titleIndex: number, title: string, rowIndex: number, row: { code: string }) => void; + handleRowSelection: (titleIndex: number, rowIndex: number) => void; toggleExpand: (expanded: boolean, titleIndex: number) => void; + replaceRowSelection: ( + event: React.MouseEvent + ) => void; + canReplace: boolean; } -const SearchResult : React.FC = ({ +const SearchResult: React.FC = ({ searchResults, unExpandedTitles, searchText, selectedRow, handleRowSelection, toggleExpand, + replaceRowSelection, + canReplace, }) => { - const renderStringWithHighlight = (str: string, highlight: string) => { - const parts = str.split(highlight); - return ( - - {parts.map((part, index) => ( - - {part} - {index !== parts.length - 1 && {highlight}} - - ))} - - ); - }; + const renderStringWithHighlight = (str: string, highlight: string) => { + const parts = str.split(highlight); return ( -
    + + {parts.map((part, index) => ( + + {part} + {index !== parts.length - 1 && ( + {highlight} + )} + + ))} + + ); + }; + return ( +
      {searchResults.map((result, titleIndex) => { return Object.keys(result ?? {}).map((title) => (
    • toggleExpand(expanded, titleIndex)} + onExpanded={(expanded: boolean) => + toggleExpand(expanded, titleIndex) + } /> -
        - {searchText && result[title].map((row, rowIndex) => ( -
      • handleRowSelection(titleIndex, title, rowIndex, row)} - > - {renderStringWithHighlight(row.code, searchText)} -
      • - ))} +
          + {searchText && + result[title].map((row, rowIndex) => ( +
        • handleRowSelection(titleIndex, rowIndex)} + > +
          + {renderStringWithHighlight(row.code, searchText)} +
          + {canReplace && + titleIndex === selectedRow.titleIndex && + rowIndex === selectedRow.rowIndex && ( +
          + +
          + )} +
        • + ))}
        )); @@ -63,4 +99,4 @@ const SearchResult : React.FC = ({ ); }; -export default SearchResult; \ No newline at end of file +export default SearchResult; diff --git a/src/components/searchtext/search-text.less b/src/components/searchtext/search-text.less index ffbe3b8..3ec9007 100644 --- a/src/components/searchtext/search-text.less +++ b/src/components/searchtext/search-text.less @@ -7,15 +7,6 @@ border: 1px solid #F0F0F0; } -.search-result-top-search { - position: sticky; - z-index: 1; - background: var(--monaco-editor-background); - display: flex; - align-items: center; - padding-right: 5px; -} - .search-result-list { margin-top: 0; overflow-y: auto; @@ -45,6 +36,21 @@ } } +.search-results-code { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + margin-left: 10px; +} + +.search-results-replace { + display: inline-block; + width: 20px; + height: 20px; + cursor: pointer; + margin: 0 5px 0 5px; +} + .search-results-title, .search-results-item-selected, .search-results-item { @@ -54,6 +60,9 @@ white-space: nowrap; overflow: hidden; text-overflow: ellipsis; + display: flex; + flex-direction: horizontal; + align-items: center; color: var(--monaco-list-focusForeground) } @@ -67,15 +76,82 @@ background: var(--monaco-list-focusBackground); color: var(--monaco-list-focusForeground) } + +.search-result-top-search { + position: sticky; + z-index: 1; + background: var(--monaco-editor-background); + display: flex; + flex-direction: horizontal; +} + +.search-result-search-innner { + display: flex; + flex: 1; + flex-direction: column; + background: var(--monaco-editor-background); +} + +.search-result-search-replace-container, +.search-result-search-container { + display: flex; + background: var(--monaco-editor-background); + display: flex; + align-items: center; + padding-right: 5px; +} + .search-result-input { flex: 1; border-radius: 2px; background: var(--monaco-list-focusBackground); border: 0.5px solid; margin: 5px; + min-width: 70px; + height: 20px; + padding-left: 5px; + color: var(--monaco-list-focusForeground) +} + +.search-result-input-replace { + flex: 1; + border-radius: 2px; + background: var(--monaco-list-focusBackground); + border: 0.5px solid; + margin: 0 5px 5px 5px; margin-right: 5px; min-width: 70px; height: 20px; padding-left: 5px; color: var(--monaco-list-focusForeground) } + +.music-monaco-editor-close, +.music-monaco-editor-replace +{ + display: flex; + justify-content: center; + align-items: center; + cursor: pointer; + width: 20px; + height: 20px; +} + +.search-result-replace-switch { + display: flex; + padding-left: 5px; + justify-content: center; + align-items: center; + cursor: pointer; +} + +.search-results-file-name { + color: var(--monaco-list-focusForeground); + font-size: 13px; +} + +.search-results-file-path { + color: var(--monaco-list-focusForeground); + margin-left: 5px; + font-size: 12px; +} \ No newline at end of file diff --git a/src/multi/Editor.tsx b/src/multi/Editor.tsx index 922d720..24053de 100644 --- a/src/multi/Editor.tsx +++ b/src/multi/Editor.tsx @@ -797,7 +797,7 @@ const MultiPrivateEditorComp = React.forwardRef< column: 1, }, }); - }, []); + }, [getAllFiles]); const configListFiles = useCallback(() => { const obj = getAllFiles(); @@ -815,6 +815,16 @@ const MultiPrivateEditorComp = React.forwardRef< return Object.keys(obj); }, [getAllFiles]); + const onReplace = useCallback((listFiles: Record) => { + const obj = getAllFiles(); + for (const key in obj) { + if (listFiles[key] !== null) { + obj[key] = listFiles[key]; + } + } + refreshFiles(obj); + }, [getAllFiles]); + return (
        setSearchTextVisible(false)} + onReplace={onReplace} + rootEl={rootRef} /> )}