diff --git a/apps/remix-ide-e2e/src/tests/search.test.ts b/apps/remix-ide-e2e/src/tests/search.test.ts index 9982747e03..8e7c7b8ba9 100644 --- a/apps/remix-ide-e2e/src/tests/search.test.ts +++ b/apps/remix-ide-e2e/src/tests/search.test.ts @@ -8,7 +8,7 @@ module.exports = { before: function (browser: NightwatchBrowser, done: VoidFunction) { init(browser, done, 'http://127.0.0.1:8080', true) }, - 'Should find text': function (browser: NightwatchBrowser) { + 'Should find text #group3': function (browser: NightwatchBrowser) { browser.waitForElementVisible('*[data-id="remixIdeSidePanel"]') .click('*[plugin="search"]').waitForElementVisible('*[id="search_input"]') .setValue('*[id="search_input"]', 'read').pause(1000) @@ -24,7 +24,7 @@ module.exports = { Array.isArray(res.value) && browser.assert.equal(res.value.length, 6) }) }, - 'Should find regex': function (browser: NightwatchBrowser) { + 'Should find regex #group3': function (browser: NightwatchBrowser) { browser .waitForElementVisible('*[data-id="search_use_regex"]').click('*[data-id="search_use_regex"]') .waitForElementVisible('*[id="search_input"]') @@ -39,7 +39,7 @@ module.exports = { Array.isArray(res.value) && browser.assert.equal(res.value.length, 4) }) }, - 'Should find matchcase': function (browser: NightwatchBrowser) { + 'Should find matchcase #group3': function (browser: NightwatchBrowser) { browser .waitForElementVisible('*[data-id="search_use_regex"]').click('*[data-id="search_use_regex"]') .waitForElementVisible('*[data-id="search_case_sensitive"]').click('*[data-id="search_case_sensitive"]') @@ -55,7 +55,7 @@ module.exports = { .waitForElementContainsText('*[data-id="search_results"]', 'DEPLOY_WEB3.JS', 60000) .waitForElementContainsText('*[data-id="search_results"]', 'scripts', 60000) }, - 'Should find matchword': function (browser: NightwatchBrowser) { + 'Should find matchword #group3': function (browser: NightwatchBrowser) { browser .waitForElementVisible('*[data-id="search_case_sensitive"]').click('*[data-id="search_case_sensitive"]') .waitForElementVisible('*[data-id="search_whole_word"]').click('*[data-id="search_whole_word"]') @@ -65,7 +65,7 @@ module.exports = { Array.isArray(res.value) && browser.assert.equal(res.value.length, 27) }) }, - 'Should replace text': function (browser: NightwatchBrowser) { + 'Should replace text #group3': function (browser: NightwatchBrowser) { browser .waitForElementVisible('*[data-id="toggle_replace"]').click('*[data-id="toggle_replace"]') .waitForElementVisible('*[id="search_replace"]') @@ -79,7 +79,7 @@ module.exports = { browser.assert.ok(content.includes('replacing deployer for a constructor'), 'should replace text ok') }) }, - 'Should replace text without confirmation': function (browser: NightwatchBrowser) { + 'Should replace text without confirmation #group3': function (browser: NightwatchBrowser) { browser.click('*[data-id="confirm_replace_label"]').pause(500) .clearValue('*[id="search_input"]') .setValue('*[id="search_input"]', 'replacing').pause(1000) @@ -92,7 +92,120 @@ module.exports = { browser.assert.ok(content.includes('replacing2 deployer for a constructor'), 'should replace text ok') }) }, - 'Should find text with include': function (browser: NightwatchBrowser) { + 'Should replace all & undo #group3': function (browser: NightwatchBrowser) { + browser + .clearValue('*[id="search_input"]') + .setValue('*[id="search_input"]', 'storage') + .clearValue('*[id="search_replace"]') + .setValue('*[id="search_replace"]', '123test').pause(1000) + .waitForElementVisible('*[data-id="replace-all-contracts/1_Storage.sol"]') + .click('*[data-id="replace-all-contracts/1_Storage.sol"]').pause(2000) + .getEditorValue((content) => { + browser.assert.ok(content.includes('contract 123test'), 'should replace text ok') + browser.assert.ok(content.includes('title 123test'), 'should replace text ok') + }) + .waitForElementVisible('*[data-id="undo-replace-contracts/1_Storage.sol"]') + .click('*[data-id="undo-replace-contracts/1_Storage.sol"]').pause(2000) + .getEditorValue((content) => { + browser.assert.ok(content.includes('contract Storage'), 'should undo text ok') + browser.assert.ok(content.includes('title Storage'), 'should undo text ok') + }) + }, + 'Should replace all & undo & switch between files #group3': function (browser: NightwatchBrowser) { + browser.waitForElementVisible('*[id="search_input"]') + .clearValue('*[id="search_input"]') + .setValue('*[id="search_input"]', 'storage') + .clearValue('*[id="search_replace"]') + .setValue('*[id="search_replace"]', '123test').pause(1000) + .waitForElementVisible('*[data-id="replace-all-contracts/1_Storage.sol"]') + .click('*[data-id="replace-all-contracts/1_Storage.sol"]').pause(2000) + .getEditorValue((content) => { + browser.assert.ok(content.includes('contract 123test'), 'should replace text ok') + browser.assert.ok(content.includes('title 123test'), 'should replace text ok') + }) + .waitForElementVisible('*[data-id="undo-replace-contracts/1_Storage.sol"]') + .openFile('README.txt') + .click('*[plugin="search"]').pause(2000) + .waitForElementNotPresent('*[data-id="undo-replace-contracts/1_Storage.sol"]') + .waitForElementVisible('*[data-id="replace-all-README.txt"]') + .click('*[data-id="replace-all-README.txt"]').pause(2000) + .getEditorValue((content) => { + browser.assert.ok(content.includes("123test' contract"), 'should replace text ok') + }) + .waitForElementVisible('*[data-id="undo-replace-README.txt"]') + .click('div[title="default_workspace/contracts/1_Storage.sol"]').pause(2000) + .waitForElementVisible('*[data-id="undo-replace-contracts/1_Storage.sol"]') + .click('*[data-id="undo-replace-contracts/1_Storage.sol"]').pause(2000) + .getEditorValue((content) => { + browser.assert.ok(content.includes('contract Storage'), 'should undo text ok') + browser.assert.ok(content.includes('title Storage'), 'should undo text ok') + }) + .click('div[title="default_workspace/README.txt"]').pause(2000) + .waitForElementVisible('*[data-id="undo-replace-README.txt"]') + .click('*[data-id="undo-replace-README.txt"]').pause(2000) + .getEditorValue((content) => { + browser.assert.ok(content.includes("Storage' contract"), 'should replace text ok') + }) + }, + 'Should hide button when edited content is the same #group3': function (browser: NightwatchBrowser) { + browser.waitForElementVisible('*[data-id="remixIdeSidePanel"]') + .addFile('test.sol', { content: '123'}) + .click('*[plugin="search"]').waitForElementVisible('*[id="search_input"]') + .clearValue('*[id="search_input"]') + .setValue('*[id="search_input"]', '123') + .clearValue('*[id="search_replace"]') + .setValue('*[id="search_replace"]', '456').pause(1000) + .waitForElementVisible('*[data-id="replace-all-test.sol"]') + .click('*[data-id="replace-all-test.sol"]').pause(2000) + .getEditorValue((content) => { + browser.assert.ok(content.includes('456'), 'should replace text ok') + } + ) + .setEditorValue('123') + .getEditorValue((content) => { + browser.assert.ok(content.includes('123'), 'should have text ok') + } + ).pause(1000) + .waitForElementNotPresent('*[data-id="undo-replace-test.sol"]') + }, + 'Should disable/enable button when edited content changed #group3': function (browser: NightwatchBrowser) { + browser + .waitForElementVisible('*[id="search_input"]') + .clearValue('*[id="search_input"]') + .setValue('*[id="search_input"]', '123') + .clearValue('*[id="search_replace"]') + .setValue('*[id="search_replace"]', 'replaced').pause(1000) + .waitForElementVisible('*[data-id="replace-all-test.sol"]') + .click('*[data-id="replace-all-test.sol"]').pause(2000) + .getEditorValue((content) => { + browser.assert.ok(content.includes('replaced'), 'should replace text ok') + } + ) + .setEditorValue('changed') + .getEditorValue((content) => { + browser.assert.ok(content.includes('changed'), 'should have text ok') + } + ).pause(1000) + .waitForElementVisible('*[data-id="undo-replace-test.sol"]') + .getAttribute('[data-id="undo-replace-test.sol"]', 'disabled', (result) => { + browser.assert.equal(result.value, 'true', 'should be disabled') + }) + .setEditorValue('replaced') + .getEditorValue((content) => { + browser.assert.ok(content.includes('replaced'), 'should have text ok') + } + ).pause(1000) + .waitForElementVisible('*[data-id="undo-replace-test.sol"]') + .getAttribute('[data-id="undo-replace-test.sol"]', 'disabled', (result) => { + browser.assert.equal(result.value, null, 'should not be disabled') + }) + .click('*[data-id="undo-replace-test.sol"]').pause(2000) + .getEditorValue((content) => { + browser.assert.ok(content.includes('123'), 'should have text ok') + }) + .waitForElementNotPresent('*[data-id="undo-replace-test.sol"]') + }, + 'Should find text with include #group3': function (browser: NightwatchBrowser) { browser .clearValue('*[id="search_input"]') .setValue('*[id="search_input"]', 'contract').pause(1000) @@ -101,7 +214,7 @@ module.exports = { Array.isArray(res.value) && browser.assert.equal(res.value.length, 4) }) }, - 'Should find text with exclude': function (browser: NightwatchBrowser) { + 'Should find text with exclude #group3': function (browser: NightwatchBrowser) { browser .clearValue('*[id="search_include"]').pause(2000) .setValue('*[id="search_include"]', '**').pause(2000) @@ -113,7 +226,7 @@ module.exports = { Array.isArray(res.value) && browser.assert.equal(res.value.length, 22) }) }, - 'should clear search': function (browser: NightwatchBrowser) { + 'should clear search #group3': function (browser: NightwatchBrowser) { browser .waitForElementVisible('*[id="search_input"]') .setValue('*[id="search_input"]', 'nodata').pause(1000) diff --git a/apps/remix-ide/src/app/tabs/search.tsx b/apps/remix-ide/src/app/tabs/search.tsx index 057653a8d8..29e6c6abe7 100644 --- a/apps/remix-ide/src/app/tabs/search.tsx +++ b/apps/remix-ide/src/app/tabs/search.tsx @@ -7,7 +7,7 @@ const profile = { displayName: 'Search in files', methods: [''], events: [], - icon: 'assets/img/Search_Icon.svg', + icon: 'assets/img/search_icon.webp', description: '', kind: '', location: 'sidePanel', diff --git a/apps/remix-ide/src/assets/img/Search_Icon.svg b/apps/remix-ide/src/assets/img/Search_Icon.svg deleted file mode 100644 index 00a6fcde04..0000000000 --- a/apps/remix-ide/src/assets/img/Search_Icon.svg +++ /dev/null @@ -1,77 +0,0 @@ - - - - - - - - - - image/svg+xml - - - - - - - - - - - - diff --git a/apps/remix-ide/src/assets/img/search_icon.webp b/apps/remix-ide/src/assets/img/search_icon.webp new file mode 100644 index 0000000000..93b2695931 Binary files /dev/null and b/apps/remix-ide/src/assets/img/search_icon.webp differ diff --git a/libs/remix-ui/search/src/lib/components/OverWriteCheck.tsx b/libs/remix-ui/search/src/lib/components/OverWriteCheck.tsx index 5a472c7881..fba80c5005 100644 --- a/libs/remix-ui/search/src/lib/components/OverWriteCheck.tsx +++ b/libs/remix-ui/search/src/lib/components/OverWriteCheck.tsx @@ -11,7 +11,7 @@ export const OverWriteCheck = props => { return ( <> {state.replaceEnabled ? ( -
+
{ const { @@ -10,20 +11,19 @@ export const Undo = () => { } = useContext(SearchContext) const { alert } = useDialogDispatchers() - const undo = async () => { try{ - await undoReplace(state.undoBuffer[0]) + await undoReplace(state.undoBuffer[`${state.workspace}/${state.currentFile}`]) }catch(e){ alert({ id: 'undo_error', title: 'Cannot undo this change', message: e.message }) } } return (<> - {state.undoBuffer && state.undoBuffer.length > 0 ? - : null} ) } \ No newline at end of file diff --git a/libs/remix-ui/search/src/lib/components/results/ResultItem.tsx b/libs/remix-ui/search/src/lib/components/results/ResultItem.tsx index b0ac872251..91fb1c6042 100644 --- a/libs/remix-ui/search/src/lib/components/results/ResultItem.tsx +++ b/libs/remix-ui/search/src/lib/components/results/ResultItem.tsx @@ -104,7 +104,11 @@ export const ResultItem = (props: ResultItemProps) => { {loading ?
Loading...
: null} {!toggleExpander && !loading ? (
- {state.replaceEnabled?
replace()} className='btn btn-secondary btn-block mb-2 btn-sm'>Replace all
:null} + {state.replaceEnabled? +
+
replace()} className='btn btn-secondary mb-2 btn-sm'>Replace all
+
+ :null} {lines.map((line, index) => ( index < state.maxLines ? { + dispatch({ + type: 'SET_CURRENT_FILE', + payload: file + }) + }, + setCurrentWorkspace: (workspace: any) => { + dispatch({ + type: 'SET_CURRENT_WORKSPACE', + payload: workspace + }) + }, updateCount: (count: number, file: string) => { dispatch({ type: 'UPDATE_COUNT', @@ -212,6 +224,16 @@ export const SearchProvider = ({ ) setUndoState(content, replaced, result.path) }, + setUndoEnabled: (path:string, workspace: string, content: string) => { + dispatch({ + type: 'SET_UNDO_ENABLED', + payload: { + path, + workspace, + content + } + }) + }, undoReplace: async (buffer: undoBufferRecord) => { const content = await plugin.call( 'fileManager', @@ -219,9 +241,7 @@ export const SearchProvider = ({ buffer.path ) if (buffer.newContent !== content) { - value.clearUndo() throw new Error('Can not undo replace, file has been changed.') - } await plugin.call( 'fileManager', @@ -234,7 +254,6 @@ export const SearchProvider = ({ 'open', buffer.path ) - value.clearUndo() }, clearUndo: () => { dispatch ({ @@ -251,13 +270,20 @@ export const SearchProvider = ({ } useEffect(() => { - plugin.on('filePanel', 'setWorkspace', () => { + plugin.on('filePanel', 'setWorkspace', async (workspace) => { value.setSearchResults(null) value.clearUndo() + value.setCurrentWorkspace(workspace.name) }) plugin.on('fileManager', 'fileSaved', async file => { await reloadStateForFile(file) + await checkUndoState(file) + }) + plugin.on('fileManager', 'currentFileChanged', async file => { + value.setCurrentFile(file) + await checkUndoState(file) }) + return () => { plugin.off('fileManager', 'fileChanged') plugin.off('filePanel', 'setWorkspace') @@ -276,13 +302,28 @@ export const SearchProvider = ({ return results } + const checkUndoState = async (path: string) => { + if (!plugin) return + try { + const content = await plugin.call( + 'fileManager', + 'readFile', + path + ) + const workspace = await plugin.call('filePanel', 'getCurrentWorkspace') + value.setUndoEnabled(path, workspace.name, content) + } catch (e) { + console.log(e) + } + } + const setUndoState = async (oldContent: string, newContent: string, path: string) => { const workspace = await plugin.call('filePanel', 'getCurrentWorkspace') const undo = { oldContent, newContent, path, - workspace + workspace: workspace.name } dispatch({ type: 'SET_UNDO', diff --git a/libs/remix-ui/search/src/lib/reducers/Reducer.ts b/libs/remix-ui/search/src/lib/reducers/Reducer.ts index b40d8ab4c6..3e188f61fb 100644 --- a/libs/remix-ui/search/src/lib/reducers/Reducer.ts +++ b/libs/remix-ui/search/src/lib/reducers/Reducer.ts @@ -41,15 +41,25 @@ export const SearchReducer = (state: SearchState = SearchingInitialState, action searchResults: action.payload, count: 0 } + case 'SET_UNDO_ENABLED': + if(state.undoBuffer[`${action.payload.workspace}/${action.payload.path}`]){ + state.undoBuffer[`${action.payload.workspace}/${action.payload.path}`].enabled = (action.payload.content === state.undoBuffer[`${action.payload.workspace}/${action.payload.path}`].newContent) + state.undoBuffer[`${action.payload.workspace}/${action.payload.path}`].visible = (action.payload.content !== state.undoBuffer[`${action.payload.workspace}/${action.payload.path}`].oldContent) + } + return { + ...state, + } case 'SET_UNDO': { const undoState = { newContent : action.payload.newContent, oldContent: action.payload.oldContent, path: action.payload.path, workspace: action.payload.workspace, - timeStamp: Date.now() + timeStamp: Date.now(), + enabled: true, + visible: true } - state.undoBuffer = [undoState] + state.undoBuffer[`${undoState.workspace}/${undoState.path}`] = undoState return { ...state, } @@ -120,6 +130,16 @@ export const SearchReducer = (state: SearchState = SearchingInitialState, action return { ...state, } + case 'SET_CURRENT_FILE': + return { + ...state, + currentFile: action.payload, + } + case 'SET_CURRENT_WORKSPACE': + return { + ...state, + workspace: action.payload, + } case 'RELOAD_FILE': if (state.searchResults) { const findFile = state.searchResults.find(file => file.filename === action.payload) diff --git a/libs/remix-ui/search/src/lib/search.css b/libs/remix-ui/search/src/lib/search.css index 1b88368bc8..acc789d51c 100644 --- a/libs/remix-ui/search/src/lib/search.css +++ b/libs/remix-ui/search/src/lib/search.css @@ -121,4 +121,16 @@ display: flex !important; align-items: center; cursor: pointer !important; +} + +.search_plugin_wrap_summary_replace { + display: flex; + flex-direction: row; + justify-content: flex-end; +} + +.undo-button { + white-space: pre; + text-overflow: ellipsis; + overflow: hidden; } \ No newline at end of file diff --git a/libs/remix-ui/search/src/lib/types/index.ts b/libs/remix-ui/search/src/lib/types/index.ts index aa11435099..231c4ebf93 100644 --- a/libs/remix-ui/search/src/lib/types/index.ts +++ b/libs/remix-ui/search/src/lib/types/index.ts @@ -40,9 +40,10 @@ export interface undoBufferRecord { path: string, newContent: string, timeStamp: number, - oldContent: string + oldContent: string, + enabled: boolean, + visible: boolean } - export interface SearchState { find: string, searchResults: SearchResult[], @@ -60,7 +61,9 @@ export interface SearchState { maxFiles: number, maxLines: number clipped: boolean, - undoBuffer: undoBufferRecord[], + undoBuffer: Record[], + currentFile: string, + workspace: string } export const SearchingInitialState: SearchState = { @@ -80,5 +83,7 @@ export const SearchingInitialState: SearchState = { maxFiles: 5000, maxLines: 5000, clipped: false, - undoBuffer: null + undoBuffer: null, + currentFile: '', + workspace: '' } \ No newline at end of file