From 4d92dd81c98b8a8dd88b8f28c3736b9b604f95b4 Mon Sep 17 00:00:00 2001 From: filip mertens Date: Fri, 11 Mar 2022 13:17:00 +0100 Subject: [PATCH 01/18] fixes --- apps/remix-ide-e2e/src/tests/search.test.ts | 2 + apps/remix-ide/src/app/editor/editor.js | 16 +++++++- apps/remix-ide/src/app/tabs/search.tsx | 2 +- .../remix-ui/editor/src/lib/actions/editor.ts | 37 +++++++++++++++++++ .../search/src/lib/components/Exclude.tsx | 4 +- .../search/src/lib/components/Find.tsx | 1 - .../src/lib/components/FindContainer.tsx | 33 +++++++++++++++++ .../search/src/lib/components/Include.tsx | 4 +- .../src/lib/components/OverWriteCheck.tsx | 36 +++++++++--------- .../search/src/lib/components/Replace.tsx | 2 +- .../search/src/lib/components/Search.tsx | 4 +- .../src/lib/components/results/ResultItem.tsx | 3 +- .../lib/components/results/ResultSummary.tsx | 7 ++-- .../src/lib/components/results/Results.tsx | 8 ++-- .../search/src/lib/context/context.tsx | 10 ++++- .../search/src/lib/reducers/Reducer.ts | 6 +++ libs/remix-ui/search/src/lib/search.css | 17 +++++++++ libs/remix-ui/search/src/lib/types/index.ts | 8 +++- 18 files changed, 163 insertions(+), 37 deletions(-) create mode 100644 libs/remix-ui/search/src/lib/components/FindContainer.tsx diff --git a/apps/remix-ide-e2e/src/tests/search.test.ts b/apps/remix-ide-e2e/src/tests/search.test.ts index 19fe83da3c..9982747e03 100644 --- a/apps/remix-ide-e2e/src/tests/search.test.ts +++ b/apps/remix-ide-e2e/src/tests/search.test.ts @@ -67,6 +67,8 @@ module.exports = { }, 'Should replace text': function (browser: NightwatchBrowser) { browser + .waitForElementVisible('*[data-id="toggle_replace"]').click('*[data-id="toggle_replace"]') + .waitForElementVisible('*[id="search_replace"]') .setValue('*[id="search_replace"]', 'replacing').pause(1000) .waitForElementVisible('*[data-id="contracts/2_Owner.sol-30-71"]') .moveToElement('*[data-id="contracts/2_Owner.sol-30-71"]', 10, 10) diff --git a/apps/remix-ide/src/app/editor/editor.js b/apps/remix-ide/src/app/editor/editor.js index 875835d8ff..247adbf391 100644 --- a/apps/remix-ide/src/app/editor/editor.js +++ b/apps/remix-ide/src/app/editor/editor.js @@ -12,7 +12,7 @@ const profile = { name: 'editor', description: 'service - editor', version: packageJson.version, - methods: ['highlight', 'discardHighlight', 'clearAnnotations', 'addAnnotation', 'gotoLine', 'getCursorPosition'] + methods: ['highlight', 'discardHighlight', 'clearAnnotations', 'addAnnotation', 'gotoLine', 'revealRange', 'getCursorPosition'] } class Editor extends Plugin { @@ -390,6 +390,20 @@ class Editor extends Plugin { this.emit('revealLine', line + 1, col) } + /** + * Reveals the range in the editor. + * @param {number} startLineNumber + * @param {number} startColumn + * @param {number} endLineNumber + * @param {number} endColumn + */ + revealRange (startLineNumber, startColumn, endLineNumber, endColumn) { + if (!this.activated) return + this.emit('focus') + console.log(startLineNumber, startColumn, endLineNumber, endColumn) + this.emit('revealRange', startLineNumber, startColumn, endLineNumber, endColumn) + } + /** * Scrolls to a line. If center is true, it puts the line in middle of screen (or attempts to). * @param {number} line The line to scroll to diff --git a/apps/remix-ide/src/app/tabs/search.tsx b/apps/remix-ide/src/app/tabs/search.tsx index 6914ed0150..057653a8d8 100644 --- a/apps/remix-ide/src/app/tabs/search.tsx +++ b/apps/remix-ide/src/app/tabs/search.tsx @@ -4,7 +4,7 @@ import React from 'react' // eslint-disable-line import { SearchTab } from '@remix-ui/search' const profile = { name: 'search', - displayName: 'Search', + displayName: 'Search in files', methods: [''], events: [], icon: 'assets/img/Search_Icon.svg', diff --git a/libs/remix-ui/editor/src/lib/actions/editor.ts b/libs/remix-ui/editor/src/lib/actions/editor.ts index 0b49c0686b..b718ffca5a 100644 --- a/libs/remix-ui/editor/src/lib/actions/editor.ts +++ b/libs/remix-ui/editor/src/lib/actions/editor.ts @@ -1,3 +1,5 @@ +import { IRange } from "monaco-editor"; + export interface Action { type: string; payload: Record @@ -49,6 +51,27 @@ export const reducerActions = (models = initialState, action: Action) => { editor.setPosition({ column, lineNumber: line }) return models } + case 'REVEAL_RANGE': { + if (!editor) return models + const range: IRange = { + startLineNumber: action.payload.startLineNumber +1, + startColumn: action.payload.startColumn, + endLineNumber: action.payload.endLineNumber + 1, + endColumn: action.payload.endColumn + } + // reset to start of line + if(action.payload.startColumn < 100){ + editor.revealRange({ + startLineNumber: range.startLineNumber, + startColumn: 1, + endLineNumber: range.endLineNumber, + endColumn: 1 + }) + }else{ + editor.revealRangeInCenter(range) + } + return models + } case 'FOCUS': { if (!editor) return models editor.focus() @@ -106,6 +129,20 @@ export const reducerListener = (plugin, dispatch, monaco, editor, events) => { }) }) + plugin.on('editor', 'revealRange', (startLineNumber, startColumn, endLineNumber, endColumn) => { + dispatch({ + type: 'REVEAL_RANGE', + payload: { + startLineNumber, + startColumn, + endLineNumber, + endColumn + }, + monaco, + editor + }) + }) + plugin.on('editor', 'focus', () => { dispatch({ type: 'FOCUS', diff --git a/libs/remix-ui/search/src/lib/components/Exclude.tsx b/libs/remix-ui/search/src/lib/components/Exclude.tsx index 36d3bdd103..c334116a6c 100644 --- a/libs/remix-ui/search/src/lib/components/Exclude.tsx +++ b/libs/remix-ui/search/src/lib/components/Exclude.tsx @@ -17,8 +17,8 @@ export const Exclude = props => { return ( <> -
- +
+ { return ( <>
-
{ + const { setReplaceEnabled } = useContext(SearchContext) + const [expanded, setExpanded] = useState(false) + const toggleExpand = () => setExpanded(!expanded) + useEffect(() => { + setReplaceEnabled(expanded) + }, [expanded]) + return ( +
+
+
+ + {expanded ? : null} +
+
+ ) +} diff --git a/libs/remix-ui/search/src/lib/components/Include.tsx b/libs/remix-ui/search/src/lib/components/Include.tsx index ab50233885..bfd0a33596 100644 --- a/libs/remix-ui/search/src/lib/components/Include.tsx +++ b/libs/remix-ui/search/src/lib/components/Include.tsx @@ -13,8 +13,8 @@ export const Include = props => { return ( <> -
- +
+ { - const { setReplaceWithoutConfirmation } = useContext(SearchContext) + const { setReplaceWithoutConfirmation, state } = useContext(SearchContext) const change = e => { setReplaceWithoutConfirmation(e.target.checked) @@ -10,23 +10,25 @@ export const OverWriteCheck = props => { return ( <> -
-
- - + {state.replaceEnabled ? ( +
+
+ + +
-
+ ) : null} ) } diff --git a/libs/remix-ui/search/src/lib/components/Replace.tsx b/libs/remix-ui/search/src/lib/components/Replace.tsx index 7c39191e6d..7bbe7f80a9 100644 --- a/libs/remix-ui/search/src/lib/components/Replace.tsx +++ b/libs/remix-ui/search/src/lib/components/Replace.tsx @@ -12,7 +12,7 @@ export const Replace = props => { return ( <>
- + { @@ -16,8 +17,7 @@ return ( <>
- - + 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 91bb40b51b..0728d220a1 100644 --- a/libs/remix-ui/search/src/lib/components/results/ResultItem.tsx +++ b/libs/remix-ui/search/src/lib/components/results/ResultItem.tsx @@ -86,12 +86,13 @@ export const ResultItem = (props: ResultItemProps) => { {!toggleExpander && !loading ? (
{lines.map((line, index) => ( + index < state.maxLines ? + />: null ))}
) : null} diff --git a/libs/remix-ui/search/src/lib/components/results/ResultSummary.tsx b/libs/remix-ui/search/src/lib/components/results/ResultSummary.tsx index 157aa43006..0d4a640a04 100644 --- a/libs/remix-ui/search/src/lib/components/results/ResultSummary.tsx +++ b/libs/remix-ui/search/src/lib/components/results/ResultSummary.tsx @@ -47,15 +47,16 @@ export const ResultSummary = (props: ResultSummaryProps) => { className='search_plugin_search_line pb-1' >
{lineItem.left.substring(lineItem.left.length - 20).trimStart()}
- {lineItem.center} - {state.replace? {state.replace}:<>} + {lineItem.center} + {state.replace && state.replaceEnabled? {state.replace}:<>}
{lineItem.right.substring(0, 100)}
+ {state.replaceEnabled?
{ replace(lineItem) }} className="codicon codicon-find-replace" role="button" aria-label="Replace" aria-disabled="false">
-
+
:null}
))} diff --git a/libs/remix-ui/search/src/lib/components/results/Results.tsx b/libs/remix-ui/search/src/lib/components/results/Results.tsx index b2094d23c9..66bee34149 100644 --- a/libs/remix-ui/search/src/lib/components/results/Results.tsx +++ b/libs/remix-ui/search/src/lib/components/results/Results.tsx @@ -5,13 +5,13 @@ import { ResultItem } from './ResultItem' export const Results = () => { const { state } = useContext(SearchContext) return ( -
+
{state.find ?
{state.count} results
: null} - {state.count < state.maxResults && state.searchResults && + {state.find && state.count >= state.maxResults?
The result set only contains a subset of all matches

Please narrow down your search.
: null} + {state.searchResults && state.searchResults.map((result, index) => { - return + return index : null })} - {state.find && state.count >= state.maxResults?
Too many results to display.

Please narrow your search.
: null}
) } diff --git a/libs/remix-ui/search/src/lib/context/context.tsx b/libs/remix-ui/search/src/lib/context/context.tsx index bbc3c72795..267fa2a92e 100644 --- a/libs/remix-ui/search/src/lib/context/context.tsx +++ b/libs/remix-ui/search/src/lib/context/context.tsx @@ -20,6 +20,7 @@ export interface SearchingStateInterface { state: SearchState setFind: (value: string) => void setReplace: (value: string) => void + setReplaceEnabled: (value: boolean) => void setInclude: (value: string) => void setExclude: (value: string) => void setCaseSensitive: (value: boolean) => void @@ -63,6 +64,12 @@ export const SearchProvider = ({ payload: value }) }, + setReplaceEnabled: (value: boolean) => { + dispatch({ + type: 'SET_REPLACE_ENABLED', + payload: value + }) + }, setInclude: (value: string) => { dispatch({ type: 'SET_INCLUDE', @@ -144,7 +151,7 @@ export const SearchProvider = ({ findText: async (path: string) => { if (!plugin) return try { - if (state.find.length < 3) return + if (state.find.length < 1) return const text = await plugin.call('fileManager', 'readFile', path) let flags = 'g' let find = state.find @@ -162,6 +169,7 @@ export const SearchProvider = ({ ) => { await plugin.call('editor', 'discardHighlight') await plugin.call('editor', 'highlight', line.position, result.path) + await plugin.call('editor', 'revealRange', line.position.start.line, line.position.start.column, line.position.end.line, line.position.end.column) }, replaceText: async (result: SearchResult, line: SearchResultLineLine) => { try { diff --git a/libs/remix-ui/search/src/lib/reducers/Reducer.ts b/libs/remix-ui/search/src/lib/reducers/Reducer.ts index 05e9efaecb..47b9cef21f 100644 --- a/libs/remix-ui/search/src/lib/reducers/Reducer.ts +++ b/libs/remix-ui/search/src/lib/reducers/Reducer.ts @@ -15,6 +15,12 @@ export const SearchReducer = (state: SearchState = SearchingInitialState, action replace: action.payload, } + case 'SET_REPLACE_ENABLED': + return { + ...state, + replaceEnabled: action.payload, + } + case 'SET_INCLUDE': return { ...state, diff --git a/libs/remix-ui/search/src/lib/search.css b/libs/remix-ui/search/src/lib/search.css index 895ce83e7f..8774dc310c 100644 --- a/libs/remix-ui/search/src/lib/search.css +++ b/libs/remix-ui/search/src/lib/search.css @@ -103,4 +103,21 @@ .search_plugin_search_tab .search_plugin_result_count_number { font-size: x-small; +} + +.search_plugin_find_container { + display: flex; + flex-direction: row; +} + +.search_plugin_find_container_internal { + display: flex; + flex-direction: column; + flex-grow: 1; +} + +.search_plugin_find_container_arrow { + display: flex !important; + align-items: center; + cursor: pointer !important; } \ 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 af76487c51..05f2200b1d 100644 --- a/libs/remix-ui/search/src/lib/types/index.ts +++ b/libs/remix-ui/search/src/lib/types/index.ts @@ -39,6 +39,7 @@ export interface SearchState { find: string, searchResults: SearchResult[], replace: string, + replaceEnabled: boolean, include: string, exclude: string, casesensitive: boolean, @@ -48,6 +49,8 @@ export interface SearchState { timeStamp: number, count: number, maxResults: number + maxFiles: number, + maxLines: number } export const SearchingInitialState: SearchState = { @@ -55,6 +58,7 @@ export const SearchingInitialState: SearchState = { replace: '', include: '', exclude: '', + replaceEnabled: false, searchResults: [], casesensitive: false, matchWord: false, @@ -62,5 +66,7 @@ export const SearchingInitialState: SearchState = { replaceWithOutConfirmation: false, timeStamp: 0, count: 0, - maxResults: 500 + maxResults: 1500, + maxFiles: 100, + maxLines: 200 } \ No newline at end of file From 87c3048166f8d510dcc97213612b2f3aaeaddf95 Mon Sep 17 00:00:00 2001 From: filip mertens Date: Fri, 11 Mar 2022 15:47:32 +0100 Subject: [PATCH 02/18] fix whitespace --- libs/remix-ui/search/src/lib/search.css | 1 + 1 file changed, 1 insertion(+) diff --git a/libs/remix-ui/search/src/lib/search.css b/libs/remix-ui/search/src/lib/search.css index 8774dc310c..ccf55adff2 100644 --- a/libs/remix-ui/search/src/lib/search.css +++ b/libs/remix-ui/search/src/lib/search.css @@ -64,6 +64,7 @@ .search_plugin_search_tab mark { padding: 0; + white-space: pre; } .search_plugin_search_tab .search_plugin_search_line_container .search_plugin_search_control { From 7cdfaf6ae8281372517c1952cacacaa9e5b1b2c4 Mon Sep 17 00:00:00 2001 From: bunsenstraat Date: Tue, 15 Mar 2022 12:41:08 +0100 Subject: [PATCH 03/18] merge --- .../search/src/lib/components/Exclude.tsx | 2 +- .../search/src/lib/components/Include.tsx | 2 +- .../search/src/lib/components/Replace.tsx | 2 +- .../search/src/lib/components/Search.tsx | 2 + .../search/src/lib/components/Undo.tsx | 29 +++++ .../src/lib/components/results/ResultItem.tsx | 26 +++- .../src/lib/components/results/Results.tsx | 4 +- .../lib/components/results/SearchHelper.ts | 3 + .../search/src/lib/context/context.tsx | 114 +++++++++++++++--- .../search/src/lib/reducers/Reducer.ts | 37 +++++- libs/remix-ui/search/src/lib/types/index.ts | 20 ++- 11 files changed, 207 insertions(+), 34 deletions(-) create mode 100644 libs/remix-ui/search/src/lib/components/Undo.tsx diff --git a/libs/remix-ui/search/src/lib/components/Exclude.tsx b/libs/remix-ui/search/src/lib/components/Exclude.tsx index c334116a6c..9047e26ce7 100644 --- a/libs/remix-ui/search/src/lib/components/Exclude.tsx +++ b/libs/remix-ui/search/src/lib/components/Exclude.tsx @@ -18,7 +18,7 @@ export const Exclude = props => { return ( <>
- + { return ( <>
- + { return ( <>
- + { @@ -21,6 +22,7 @@ return ( +
diff --git a/libs/remix-ui/search/src/lib/components/Undo.tsx b/libs/remix-ui/search/src/lib/components/Undo.tsx new file mode 100644 index 0000000000..e44cd23c88 --- /dev/null +++ b/libs/remix-ui/search/src/lib/components/Undo.tsx @@ -0,0 +1,29 @@ +import { useDialogDispatchers } from "@remix-ui/app" +import React from "react" +import { useContext } from "react" +import { SearchContext } from "../context/context" + +export const Undo = () => { + const { + state, + undoReplace + } = useContext(SearchContext) + const { alert } = useDialogDispatchers() + + + const undo = async () => { + try{ + await undoReplace(state.undoBuffer[0]) + }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 0728d220a1..c0d1ee1ff6 100644 --- a/libs/remix-ui/search/src/lib/components/results/ResultItem.tsx +++ b/libs/remix-ui/search/src/lib/components/results/ResultItem.tsx @@ -1,3 +1,4 @@ +import { useDialogDispatchers } from '@remix-ui/app' import React, { useContext, useEffect, useRef, useState } from 'react' import { SearchContext } from '../../context/context' import { SearchResult, SearchResultLine } from '../../types' @@ -9,7 +10,7 @@ interface ResultItemProps { } export const ResultItem = (props: ResultItemProps) => { - const { state, findText, disableForceReload, updateCount } = useContext( + const { state, findText, disableForceReload, updateCount, replaceAllInFile } = useContext( SearchContext ) const [loading, setLoading] = useState(false) @@ -17,7 +18,7 @@ export const ResultItem = (props: ResultItemProps) => { const [toggleExpander, setToggleExpander] = useState(false) const reloadTimeOut = useRef(null) const subscribed = useRef(true) - + const { modal } = useDialogDispatchers() useEffect(() => { reload() @@ -41,10 +42,28 @@ export const ResultItem = (props: ResultItemProps) => { useEffect(() => { subscribed.current = true return () => { + updateCount(0, props.file.filename) subscribed.current = false } }, []) + const confirmReplace = async () => { + setLoading(true) + try { + await replaceAllInFile(props.file) + } catch (e) { + } + setLoading(false) + } + + const replace = async () => { + if(state.replaceWithOutConfirmation){ + confirmReplace() + }else{ + modal({ id: 'confirmreplace', title: 'Replace', message: `Are you sure you want to replace '${state.find}' by '${state.replace}' in ${props.file.filename}?`, okLabel: 'Yes', okFn: confirmReplace, cancelLabel: 'No', cancelFn: ()=>{}, data: null }) + } + } + const reload = () => { findText(props.file.filename).then(res => { if (subscribed.current) { @@ -85,7 +104,8 @@ export const ResultItem = (props: ResultItemProps) => { {loading ?
Loading...
: null} {!toggleExpander && !loading ? (
- {lines.map((line, index) => ( + {state.replaceEnabled?
replace()} className='btn btn-primary btn-block mb-2 btn-sm'>Replace all
:null} + {lines.map((line, index) => ( index < state.maxLines ? { const { state } = useContext(SearchContext) return (
- {state.find ?
{state.count} results
: null} - {state.find && state.count >= state.maxResults?
The result set only contains a subset of all matches

Please narrow down your search.
: null} + {state.find ?
showing {state.count} results {state.fileCount} in files
: null} + {state.find && state.clipped?
The result set only shows a subset of all matches

Please narrow down your search.
: null} {state.searchResults && state.searchResults.map((result, index) => { return index : null diff --git a/libs/remix-ui/search/src/lib/components/results/SearchHelper.ts b/libs/remix-ui/search/src/lib/components/results/SearchHelper.ts index 88ae6b1de1..fb6b096df2 100644 --- a/libs/remix-ui/search/src/lib/components/results/SearchHelper.ts +++ b/libs/remix-ui/search/src/lib/components/results/SearchHelper.ts @@ -92,6 +92,9 @@ function getEOL(text) { return u > w ? '\n' : '\r\n'; } +export const replaceAllInFile = (string: string, re:RegExp, newText: string) => { + return string.replace(re, newText) +} export const replaceTextInLine = (str: string, searchResultLine: SearchResultLineLine, newText: string) => { return str diff --git a/libs/remix-ui/search/src/lib/context/context.tsx b/libs/remix-ui/search/src/lib/context/context.tsx index 267fa2a92e..9d131ba620 100644 --- a/libs/remix-ui/search/src/lib/context/context.tsx +++ b/libs/remix-ui/search/src/lib/context/context.tsx @@ -3,6 +3,7 @@ import { createContext, useReducer } from 'react' import { findLinesInStringWithMatch, getDirectory, + replaceAllInFile, replaceTextInLine } from '../components/results/SearchHelper' import { SearchReducer } from '../reducers/Reducer' @@ -11,7 +12,8 @@ import { SearchResult, SearchResultLine, SearchResultLineLine, - SearchingInitialState + SearchingInitialState, + undoBufferRecord } from '../types' import { filePathFilter } from '@jsdevtools/file-path-filter' import { escapeRegExp } from 'lodash' @@ -29,7 +31,7 @@ export interface SearchingStateInterface { setSearchResults: (value: SearchResult[]) => void findText: (path: string) => Promise hightLightInPath: (result: SearchResult, line: SearchResultLineLine) => void - replaceText: (result: SearchResult, line: SearchResultLineLine) => void + replaceText: (result: SearchResult, line: SearchResultLineLine) => Promise reloadFile: (file: string) => void toggleCaseSensitive: () => void toggleMatchWholeWord: () => void @@ -37,6 +39,9 @@ export interface SearchingStateInterface { setReplaceWithoutConfirmation: (value: boolean) => void disableForceReload: (file: string) => void updateCount: (count: number, file: string) => void + replaceAllInFile: (result: SearchResult) => Promise + undoReplace: (buffer: undoBufferRecord) => Promise + clearUndo: () => void } export const SearchContext = createContext(null) @@ -145,7 +150,7 @@ export const SearchProvider = ({ updateCount: (count: number, file: string) => { dispatch({ type: 'UPDATE_COUNT', - payload: {count, file} + payload: { count, file } }) }, findText: async (path: string) => { @@ -153,15 +158,9 @@ export const SearchProvider = ({ try { if (state.find.length < 1) return const text = await plugin.call('fileManager', 'readFile', path) - let flags = 'g' - let find = state.find - if (!state.casesensitive) flags += 'i' - if (!state.useRegExp) find = escapeRegExp(find) - if (state.matchWord) find = `\\b${find}\\b` - const re = new RegExp(find, flags) - const result: SearchResultLine[] = findLinesInStringWithMatch(text, re) + const result: SearchResultLine[] = findLinesInStringWithMatch(text, createRegExFromFind()) return result - } catch (e) {} + } catch (e) { } }, hightLightInPath: async ( result: SearchResult, @@ -180,26 +179,81 @@ export const SearchProvider = ({ 'readFile', result.path ) - + const replaced = replaceTextInLine(content, line, state.replace) await plugin.call( 'fileManager', 'setFile', result.path, - replaceTextInLine(content, line, state.replace) + replaced ) + setUndoState(content, replaced, result.path) } catch (e) { throw new Error(e) } + }, + replaceAllInFile: async (result: SearchResult) => { + await plugin.call('editor', 'discardHighlight') + const content = await plugin.call( + 'fileManager', + 'readFile', + result.path + ) + const replaced = replaceAllInFile(content, createRegExFromFind(), state.replace) + await plugin.call( + 'fileManager', + 'setFile', + result.path, + replaced + ) + await plugin.call( + 'fileManager', + 'open', + result.path + ) + setUndoState(content, replaced, result.path) + }, + undoReplace: async (buffer: undoBufferRecord) => { + const content = await plugin.call( + 'fileManager', + 'readFile', + buffer.path + ) + if (buffer.newContent !== content) { + value.clearUndo() + throw new Error('Can not undo replace, file has been changed.') + + } + await plugin.call( + 'fileManager', + 'setFile', + buffer.path, + buffer.oldContent + ) + await plugin.call( + 'fileManager', + 'open', + buffer.path + ) + value.clearUndo() + }, + clearUndo: () => { + dispatch ({ + type: 'CLEAR_UNDO', + payload: undefined + }) } } + + const reloadStateForFile = async (file: string) => { - await value.reloadFile(file) + await value.reloadFile(file) } useEffect(() => { plugin.on('filePanel', 'setWorkspace', () => { value.setSearchResults(null) + value.clearUndo() }) plugin.on('fileManager', 'fileSaved', async file => { await reloadStateForFile(file) @@ -215,22 +269,46 @@ export const SearchProvider = ({ const results = [] paths.split(',').forEach(path => { path = path.trim() - if(path.startsWith('*.')) path = path.replace(/(\*\.)/g, '**/*.') - if(path.endsWith('/*') && !path.endsWith('/**/*')) path = path.replace(/(\*)/g, '**/*.*') + if (path.startsWith('*.')) path = path.replace(/(\*\.)/g, '**/*.') + if (path.endsWith('/*') && !path.endsWith('/**/*')) path = path.replace(/(\*)/g, '**/*.*') results.push(path) }) return results } + const setUndoState = async (oldContent: string, newContent: string, path: string) => { + const workspace = await plugin.call('filePanel', 'getCurrentWorkspace') + const undo = { + oldContent, + newContent, + path, + workspace + } + dispatch({ + type: 'SET_UNDO', + payload: undo + }) + } + + const createRegExFromFind = () => { + let flags = 'g' + let find = state.find + if (!state.casesensitive) flags += 'i' + if (!state.useRegExp) find = escapeRegExp(find) + if (state.matchWord) find = `\\b${find}\\b` + const re = new RegExp(find, flags) + return re + } + useEffect(() => { if (state.find) { (async () => { const files = await getDirectory('/', plugin) const pathFilter: any = {} - if (state.include){ + if (state.include) { pathFilter.include = setGlobalExpression(state.include) } - if (state.exclude){ + if (state.exclude) { pathFilter.exclude = setGlobalExpression(state.exclude) } const filteredFiles = files.filter(filePathFilter(pathFilter)).map(file => { diff --git a/libs/remix-ui/search/src/lib/reducers/Reducer.ts b/libs/remix-ui/search/src/lib/reducers/Reducer.ts index 47b9cef21f..b40d8ab4c6 100644 --- a/libs/remix-ui/search/src/lib/reducers/Reducer.ts +++ b/libs/remix-ui/search/src/lib/reducers/Reducer.ts @@ -1,4 +1,4 @@ -import { Action, SearchingInitialState, SearchState } from "../types" +import { Action, SearchingInitialState, SearchState, undoBufferRecord } from "../types" export const SearchReducer = (state: SearchState = SearchingInitialState, action: Action) => { switch (action.type) { @@ -41,21 +41,50 @@ export const SearchReducer = (state: SearchState = SearchingInitialState, action searchResults: action.payload, count: 0 } + case 'SET_UNDO': { + const undoState = { + newContent : action.payload.newContent, + oldContent: action.payload.oldContent, + path: action.payload.path, + workspace: action.payload.workspace, + timeStamp: Date.now() + } + state.undoBuffer = [undoState] + return { + ...state, + } + } + case 'CLEAR_UNDO': { + state.undoBuffer = [] + return { + ...state, + } + } case 'UPDATE_COUNT': if (state.searchResults) { const findFile = state.searchResults.find(file => file.filename === action.payload.file) let count = 0 + let fileCount = 0 + let clipped = false if (findFile) { findFile.count = action.payload.count } state.searchResults.forEach(file => { - if (file.count) { - count += file.count + if (file.count) { + if(file.count > state.maxLines) { + clipped = true + count += state.maxLines + }else{ + count += file.count + } + fileCount++ } }) return { ...state, - count: count + count: count, + fileCount, + clipped } } else { return state diff --git a/libs/remix-ui/search/src/lib/types/index.ts b/libs/remix-ui/search/src/lib/types/index.ts index 05f2200b1d..aa11435099 100644 --- a/libs/remix-ui/search/src/lib/types/index.ts +++ b/libs/remix-ui/search/src/lib/types/index.ts @@ -35,6 +35,14 @@ export interface SearchResult { count: number } +export interface undoBufferRecord { + workspace: string, + path: string, + newContent: string, + timeStamp: number, + oldContent: string +} + export interface SearchState { find: string, searchResults: SearchResult[], @@ -48,9 +56,11 @@ export interface SearchState { useRegExp: boolean, timeStamp: number, count: number, - maxResults: number + fileCount: number, maxFiles: number, maxLines: number + clipped: boolean, + undoBuffer: undoBufferRecord[], } export const SearchingInitialState: SearchState = { @@ -66,7 +76,9 @@ export const SearchingInitialState: SearchState = { replaceWithOutConfirmation: false, timeStamp: 0, count: 0, - maxResults: 1500, - maxFiles: 100, - maxLines: 200 + fileCount: 0, + maxFiles: 5000, + maxLines: 5000, + clipped: false, + undoBuffer: null } \ No newline at end of file From 2eeaee4ab83d4f3c1fa306c67688032dc9875e7b Mon Sep 17 00:00:00 2001 From: bunsenstraat Date: Wed, 16 Mar 2022 09:54:43 +0100 Subject: [PATCH 04/18] layout --- libs/remix-ui/search/src/lib/components/Replace.tsx | 2 +- libs/remix-ui/search/src/lib/components/Undo.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/remix-ui/search/src/lib/components/Replace.tsx b/libs/remix-ui/search/src/lib/components/Replace.tsx index 33d414d4e7..a8bd50177f 100644 --- a/libs/remix-ui/search/src/lib/components/Replace.tsx +++ b/libs/remix-ui/search/src/lib/components/Replace.tsx @@ -11,7 +11,7 @@ export const Replace = props => { return ( <> -
+
{ return (<> {state.undoBuffer && state.undoBuffer.length > 0 ? - : null} From 015f9afa86227639d5dc51842ae5359ad6f341d0 Mon Sep 17 00:00:00 2001 From: bunsenstraat Date: Wed, 16 Mar 2022 09:54:50 +0100 Subject: [PATCH 05/18] layout --- libs/remix-ui/search/src/lib/components/Exclude.tsx | 2 +- libs/remix-ui/search/src/lib/components/FindContainer.tsx | 4 +++- libs/remix-ui/search/src/lib/components/Search.tsx | 3 +-- .../remix-ui/search/src/lib/components/results/ResultItem.tsx | 4 ++-- libs/remix-ui/search/src/lib/search.css | 2 +- 5 files changed, 8 insertions(+), 7 deletions(-) diff --git a/libs/remix-ui/search/src/lib/components/Exclude.tsx b/libs/remix-ui/search/src/lib/components/Exclude.tsx index 9047e26ce7..5385783324 100644 --- a/libs/remix-ui/search/src/lib/components/Exclude.tsx +++ b/libs/remix-ui/search/src/lib/components/Exclude.tsx @@ -18,7 +18,7 @@ export const Exclude = props => { return ( <>
- + { @@ -26,7 +27,8 @@ export const FindContainer = props => { >
- {expanded ? : null} + {expanded ? + <> : null}
) diff --git a/libs/remix-ui/search/src/lib/components/Search.tsx b/libs/remix-ui/search/src/lib/components/Search.tsx index a8f32ce4d5..7f6d62aa5e 100644 --- a/libs/remix-ui/search/src/lib/components/Search.tsx +++ b/libs/remix-ui/search/src/lib/components/Search.tsx @@ -16,12 +16,11 @@ const plugin = props.plugin return ( <> -
+
- 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 c0d1ee1ff6..b0ac872251 100644 --- a/libs/remix-ui/search/src/lib/components/results/ResultItem.tsx +++ b/libs/remix-ui/search/src/lib/components/results/ResultItem.tsx @@ -103,8 +103,8 @@ export const ResultItem = (props: ResultItemProps) => {
{loading ?
Loading...
: null} {!toggleExpander && !loading ? ( -
- {state.replaceEnabled?
replace()} className='btn btn-primary btn-block mb-2 btn-sm'>Replace all
:null} +
+ {state.replaceEnabled?
replace()} className='btn btn-secondary btn-block mb-2 btn-sm'>Replace all
:null} {lines.map((line, index) => ( index < state.maxLines ? Date: Wed, 16 Mar 2022 16:00:38 +0100 Subject: [PATCH 06/18] multi undo --- apps/remix-ide-e2e/src/tests/search.test.ts | 131 ++++++++++++++++-- apps/remix-ide/src/app/tabs/search.tsx | 2 +- apps/remix-ide/src/assets/img/Search_Icon.svg | 77 ---------- .../remix-ide/src/assets/img/search_icon.webp | Bin 0 -> 44302 bytes .../src/lib/components/OverWriteCheck.tsx | 2 +- .../search/src/lib/components/Undo.tsx | 10 +- .../src/lib/components/results/ResultItem.tsx | 6 +- .../search/src/lib/context/context.tsx | 51 ++++++- .../search/src/lib/reducers/Reducer.ts | 24 +++- libs/remix-ui/search/src/lib/search.css | 12 ++ libs/remix-ui/search/src/lib/types/index.ts | 13 +- 11 files changed, 223 insertions(+), 105 deletions(-) delete mode 100644 apps/remix-ide/src/assets/img/Search_Icon.svg create mode 100644 apps/remix-ide/src/assets/img/search_icon.webp 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 0000000000000000000000000000000000000000..93b26959314d26466576dde0c4684a399a466eeb GIT binary patch literal 44302 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®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 z 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 Lfya6nfWr7cd%yp$ literal 0 HcmV?d00001 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 From f6ede7ce0d04fe5c2141bf3cda3b80949b1a96e8 Mon Sep 17 00:00:00 2001 From: filip mertens Date: Wed, 16 Mar 2022 16:02:27 +0100 Subject: [PATCH 07/18] rm grouping --- apps/remix-ide-e2e/src/tests/search.test.ts | 26 ++++++++++----------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/apps/remix-ide-e2e/src/tests/search.test.ts b/apps/remix-ide-e2e/src/tests/search.test.ts index 8e7c7b8ba9..ce754fc425 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 #group3': function (browser: NightwatchBrowser) { + 'Should find text': 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 #group3': function (browser: NightwatchBrowser) { + 'Should find regex': 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 #group3': function (browser: NightwatchBrowser) { + 'Should find matchcase': 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 #group3': function (browser: NightwatchBrowser) { + 'Should find matchword': 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 #group3': function (browser: NightwatchBrowser) { + 'Should replace text': 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 #group3': function (browser: NightwatchBrowser) { + 'Should replace text without confirmation': 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,7 @@ module.exports = { browser.assert.ok(content.includes('replacing2 deployer for a constructor'), 'should replace text ok') }) }, - 'Should replace all & undo #group3': function (browser: NightwatchBrowser) { + 'Should replace all & undo': function (browser: NightwatchBrowser) { browser .clearValue('*[id="search_input"]') .setValue('*[id="search_input"]', 'storage') @@ -111,7 +111,7 @@ module.exports = { browser.assert.ok(content.includes('title Storage'), 'should undo text ok') }) }, - 'Should replace all & undo & switch between files #group3': function (browser: NightwatchBrowser) { + 'Should replace all & undo & switch between files': function (browser: NightwatchBrowser) { browser.waitForElementVisible('*[id="search_input"]') .clearValue('*[id="search_input"]') .setValue('*[id="search_input"]', 'storage') @@ -147,7 +147,7 @@ module.exports = { browser.assert.ok(content.includes("Storage' contract"), 'should replace text ok') }) }, - 'Should hide button when edited content is the same #group3': function (browser: NightwatchBrowser) { + 'Should hide button when edited content is the same': function (browser: NightwatchBrowser) { browser.waitForElementVisible('*[data-id="remixIdeSidePanel"]') .addFile('test.sol', { content: '123'}) .click('*[plugin="search"]').waitForElementVisible('*[id="search_input"]') @@ -168,7 +168,7 @@ module.exports = { ).pause(1000) .waitForElementNotPresent('*[data-id="undo-replace-test.sol"]') }, - 'Should disable/enable button when edited content changed #group3': function (browser: NightwatchBrowser) { + 'Should disable/enable button when edited content changed': function (browser: NightwatchBrowser) { browser .waitForElementVisible('*[id="search_input"]') .clearValue('*[id="search_input"]') @@ -205,7 +205,7 @@ module.exports = { }) .waitForElementNotPresent('*[data-id="undo-replace-test.sol"]') }, - 'Should find text with include #group3': function (browser: NightwatchBrowser) { + 'Should find text with include': function (browser: NightwatchBrowser) { browser .clearValue('*[id="search_input"]') .setValue('*[id="search_input"]', 'contract').pause(1000) @@ -214,7 +214,7 @@ module.exports = { Array.isArray(res.value) && browser.assert.equal(res.value.length, 4) }) }, - 'Should find text with exclude #group3': function (browser: NightwatchBrowser) { + 'Should find text with exclude': function (browser: NightwatchBrowser) { browser .clearValue('*[id="search_include"]').pause(2000) .setValue('*[id="search_include"]', '**').pause(2000) @@ -226,7 +226,7 @@ module.exports = { Array.isArray(res.value) && browser.assert.equal(res.value.length, 22) }) }, - 'should clear search #group3': function (browser: NightwatchBrowser) { + 'should clear search': function (browser: NightwatchBrowser) { browser .waitForElementVisible('*[id="search_input"]') .setValue('*[id="search_input"]', 'nodata').pause(1000) From df1d9f28360a47aa4b8ea59e04370d578be96e9f Mon Sep 17 00:00:00 2001 From: filip mertens Date: Wed, 16 Mar 2022 16:04:30 +0100 Subject: [PATCH 08/18] space --- libs/remix-ui/search/src/lib/components/Undo.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/remix-ui/search/src/lib/components/Undo.tsx b/libs/remix-ui/search/src/lib/components/Undo.tsx index e920a3671d..03cbbf4550 100644 --- a/libs/remix-ui/search/src/lib/components/Undo.tsx +++ b/libs/remix-ui/search/src/lib/components/Undo.tsx @@ -26,4 +26,4 @@ export const Undo = () => { Undo changes to {path.basename(state.undoBuffer[`${state.workspace}/${state.currentFile}`].path)} : null} ) -} \ No newline at end of file +} From b5682bb35aa5bc41014ea63a7c7df7e010e75c23 Mon Sep 17 00:00:00 2001 From: filip mertens Date: Fri, 11 Mar 2022 13:17:00 +0100 Subject: [PATCH 09/18] fixes --- apps/remix-ide-e2e/src/tests/search.test.ts | 2 + apps/remix-ide/src/app/editor/editor.js | 16 +++++++- apps/remix-ide/src/app/tabs/search.tsx | 2 +- .../remix-ui/editor/src/lib/actions/editor.ts | 37 +++++++++++++++++++ .../search/src/lib/components/Exclude.tsx | 4 +- .../search/src/lib/components/Find.tsx | 1 - .../src/lib/components/FindContainer.tsx | 33 +++++++++++++++++ .../search/src/lib/components/Include.tsx | 4 +- .../src/lib/components/OverWriteCheck.tsx | 36 +++++++++--------- .../search/src/lib/components/Replace.tsx | 2 +- .../search/src/lib/components/Search.tsx | 4 +- .../src/lib/components/results/ResultItem.tsx | 3 +- .../lib/components/results/ResultSummary.tsx | 7 ++-- .../src/lib/components/results/Results.tsx | 8 ++-- .../search/src/lib/context/context.tsx | 10 ++++- .../search/src/lib/reducers/Reducer.ts | 6 +++ libs/remix-ui/search/src/lib/search.css | 17 +++++++++ libs/remix-ui/search/src/lib/types/index.ts | 8 +++- 18 files changed, 163 insertions(+), 37 deletions(-) create mode 100644 libs/remix-ui/search/src/lib/components/FindContainer.tsx diff --git a/apps/remix-ide-e2e/src/tests/search.test.ts b/apps/remix-ide-e2e/src/tests/search.test.ts index 19fe83da3c..9982747e03 100644 --- a/apps/remix-ide-e2e/src/tests/search.test.ts +++ b/apps/remix-ide-e2e/src/tests/search.test.ts @@ -67,6 +67,8 @@ module.exports = { }, 'Should replace text': function (browser: NightwatchBrowser) { browser + .waitForElementVisible('*[data-id="toggle_replace"]').click('*[data-id="toggle_replace"]') + .waitForElementVisible('*[id="search_replace"]') .setValue('*[id="search_replace"]', 'replacing').pause(1000) .waitForElementVisible('*[data-id="contracts/2_Owner.sol-30-71"]') .moveToElement('*[data-id="contracts/2_Owner.sol-30-71"]', 10, 10) diff --git a/apps/remix-ide/src/app/editor/editor.js b/apps/remix-ide/src/app/editor/editor.js index 875835d8ff..247adbf391 100644 --- a/apps/remix-ide/src/app/editor/editor.js +++ b/apps/remix-ide/src/app/editor/editor.js @@ -12,7 +12,7 @@ const profile = { name: 'editor', description: 'service - editor', version: packageJson.version, - methods: ['highlight', 'discardHighlight', 'clearAnnotations', 'addAnnotation', 'gotoLine', 'getCursorPosition'] + methods: ['highlight', 'discardHighlight', 'clearAnnotations', 'addAnnotation', 'gotoLine', 'revealRange', 'getCursorPosition'] } class Editor extends Plugin { @@ -390,6 +390,20 @@ class Editor extends Plugin { this.emit('revealLine', line + 1, col) } + /** + * Reveals the range in the editor. + * @param {number} startLineNumber + * @param {number} startColumn + * @param {number} endLineNumber + * @param {number} endColumn + */ + revealRange (startLineNumber, startColumn, endLineNumber, endColumn) { + if (!this.activated) return + this.emit('focus') + console.log(startLineNumber, startColumn, endLineNumber, endColumn) + this.emit('revealRange', startLineNumber, startColumn, endLineNumber, endColumn) + } + /** * Scrolls to a line. If center is true, it puts the line in middle of screen (or attempts to). * @param {number} line The line to scroll to diff --git a/apps/remix-ide/src/app/tabs/search.tsx b/apps/remix-ide/src/app/tabs/search.tsx index 6914ed0150..057653a8d8 100644 --- a/apps/remix-ide/src/app/tabs/search.tsx +++ b/apps/remix-ide/src/app/tabs/search.tsx @@ -4,7 +4,7 @@ import React from 'react' // eslint-disable-line import { SearchTab } from '@remix-ui/search' const profile = { name: 'search', - displayName: 'Search', + displayName: 'Search in files', methods: [''], events: [], icon: 'assets/img/Search_Icon.svg', diff --git a/libs/remix-ui/editor/src/lib/actions/editor.ts b/libs/remix-ui/editor/src/lib/actions/editor.ts index 0b49c0686b..b718ffca5a 100644 --- a/libs/remix-ui/editor/src/lib/actions/editor.ts +++ b/libs/remix-ui/editor/src/lib/actions/editor.ts @@ -1,3 +1,5 @@ +import { IRange } from "monaco-editor"; + export interface Action { type: string; payload: Record @@ -49,6 +51,27 @@ export const reducerActions = (models = initialState, action: Action) => { editor.setPosition({ column, lineNumber: line }) return models } + case 'REVEAL_RANGE': { + if (!editor) return models + const range: IRange = { + startLineNumber: action.payload.startLineNumber +1, + startColumn: action.payload.startColumn, + endLineNumber: action.payload.endLineNumber + 1, + endColumn: action.payload.endColumn + } + // reset to start of line + if(action.payload.startColumn < 100){ + editor.revealRange({ + startLineNumber: range.startLineNumber, + startColumn: 1, + endLineNumber: range.endLineNumber, + endColumn: 1 + }) + }else{ + editor.revealRangeInCenter(range) + } + return models + } case 'FOCUS': { if (!editor) return models editor.focus() @@ -106,6 +129,20 @@ export const reducerListener = (plugin, dispatch, monaco, editor, events) => { }) }) + plugin.on('editor', 'revealRange', (startLineNumber, startColumn, endLineNumber, endColumn) => { + dispatch({ + type: 'REVEAL_RANGE', + payload: { + startLineNumber, + startColumn, + endLineNumber, + endColumn + }, + monaco, + editor + }) + }) + plugin.on('editor', 'focus', () => { dispatch({ type: 'FOCUS', diff --git a/libs/remix-ui/search/src/lib/components/Exclude.tsx b/libs/remix-ui/search/src/lib/components/Exclude.tsx index 36d3bdd103..c334116a6c 100644 --- a/libs/remix-ui/search/src/lib/components/Exclude.tsx +++ b/libs/remix-ui/search/src/lib/components/Exclude.tsx @@ -17,8 +17,8 @@ export const Exclude = props => { return ( <> -
- +
+ { return ( <>
-
{ + const { setReplaceEnabled } = useContext(SearchContext) + const [expanded, setExpanded] = useState(false) + const toggleExpand = () => setExpanded(!expanded) + useEffect(() => { + setReplaceEnabled(expanded) + }, [expanded]) + return ( +
+
+
+ + {expanded ? : null} +
+
+ ) +} diff --git a/libs/remix-ui/search/src/lib/components/Include.tsx b/libs/remix-ui/search/src/lib/components/Include.tsx index ab50233885..bfd0a33596 100644 --- a/libs/remix-ui/search/src/lib/components/Include.tsx +++ b/libs/remix-ui/search/src/lib/components/Include.tsx @@ -13,8 +13,8 @@ export const Include = props => { return ( <> -
- +
+ { - const { setReplaceWithoutConfirmation } = useContext(SearchContext) + const { setReplaceWithoutConfirmation, state } = useContext(SearchContext) const change = e => { setReplaceWithoutConfirmation(e.target.checked) @@ -10,23 +10,25 @@ export const OverWriteCheck = props => { return ( <> -
-
- - + {state.replaceEnabled ? ( +
+
+ + +
-
+ ) : null} ) } diff --git a/libs/remix-ui/search/src/lib/components/Replace.tsx b/libs/remix-ui/search/src/lib/components/Replace.tsx index 7c39191e6d..7bbe7f80a9 100644 --- a/libs/remix-ui/search/src/lib/components/Replace.tsx +++ b/libs/remix-ui/search/src/lib/components/Replace.tsx @@ -12,7 +12,7 @@ export const Replace = props => { return ( <>
- + { @@ -16,8 +17,7 @@ return ( <>
- - + 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 91bb40b51b..0728d220a1 100644 --- a/libs/remix-ui/search/src/lib/components/results/ResultItem.tsx +++ b/libs/remix-ui/search/src/lib/components/results/ResultItem.tsx @@ -86,12 +86,13 @@ export const ResultItem = (props: ResultItemProps) => { {!toggleExpander && !loading ? (
{lines.map((line, index) => ( + index < state.maxLines ? + />: null ))}
) : null} diff --git a/libs/remix-ui/search/src/lib/components/results/ResultSummary.tsx b/libs/remix-ui/search/src/lib/components/results/ResultSummary.tsx index 157aa43006..0d4a640a04 100644 --- a/libs/remix-ui/search/src/lib/components/results/ResultSummary.tsx +++ b/libs/remix-ui/search/src/lib/components/results/ResultSummary.tsx @@ -47,15 +47,16 @@ export const ResultSummary = (props: ResultSummaryProps) => { className='search_plugin_search_line pb-1' >
{lineItem.left.substring(lineItem.left.length - 20).trimStart()}
- {lineItem.center} - {state.replace? {state.replace}:<>} + {lineItem.center} + {state.replace && state.replaceEnabled? {state.replace}:<>}
{lineItem.right.substring(0, 100)}
+ {state.replaceEnabled?
{ replace(lineItem) }} className="codicon codicon-find-replace" role="button" aria-label="Replace" aria-disabled="false">
-
+
:null}
))} diff --git a/libs/remix-ui/search/src/lib/components/results/Results.tsx b/libs/remix-ui/search/src/lib/components/results/Results.tsx index b2094d23c9..66bee34149 100644 --- a/libs/remix-ui/search/src/lib/components/results/Results.tsx +++ b/libs/remix-ui/search/src/lib/components/results/Results.tsx @@ -5,13 +5,13 @@ import { ResultItem } from './ResultItem' export const Results = () => { const { state } = useContext(SearchContext) return ( -
+
{state.find ?
{state.count} results
: null} - {state.count < state.maxResults && state.searchResults && + {state.find && state.count >= state.maxResults?
The result set only contains a subset of all matches

Please narrow down your search.
: null} + {state.searchResults && state.searchResults.map((result, index) => { - return + return index : null })} - {state.find && state.count >= state.maxResults?
Too many results to display.

Please narrow your search.
: null}
) } diff --git a/libs/remix-ui/search/src/lib/context/context.tsx b/libs/remix-ui/search/src/lib/context/context.tsx index bbc3c72795..267fa2a92e 100644 --- a/libs/remix-ui/search/src/lib/context/context.tsx +++ b/libs/remix-ui/search/src/lib/context/context.tsx @@ -20,6 +20,7 @@ export interface SearchingStateInterface { state: SearchState setFind: (value: string) => void setReplace: (value: string) => void + setReplaceEnabled: (value: boolean) => void setInclude: (value: string) => void setExclude: (value: string) => void setCaseSensitive: (value: boolean) => void @@ -63,6 +64,12 @@ export const SearchProvider = ({ payload: value }) }, + setReplaceEnabled: (value: boolean) => { + dispatch({ + type: 'SET_REPLACE_ENABLED', + payload: value + }) + }, setInclude: (value: string) => { dispatch({ type: 'SET_INCLUDE', @@ -144,7 +151,7 @@ export const SearchProvider = ({ findText: async (path: string) => { if (!plugin) return try { - if (state.find.length < 3) return + if (state.find.length < 1) return const text = await plugin.call('fileManager', 'readFile', path) let flags = 'g' let find = state.find @@ -162,6 +169,7 @@ export const SearchProvider = ({ ) => { await plugin.call('editor', 'discardHighlight') await plugin.call('editor', 'highlight', line.position, result.path) + await plugin.call('editor', 'revealRange', line.position.start.line, line.position.start.column, line.position.end.line, line.position.end.column) }, replaceText: async (result: SearchResult, line: SearchResultLineLine) => { try { diff --git a/libs/remix-ui/search/src/lib/reducers/Reducer.ts b/libs/remix-ui/search/src/lib/reducers/Reducer.ts index 05e9efaecb..47b9cef21f 100644 --- a/libs/remix-ui/search/src/lib/reducers/Reducer.ts +++ b/libs/remix-ui/search/src/lib/reducers/Reducer.ts @@ -15,6 +15,12 @@ export const SearchReducer = (state: SearchState = SearchingInitialState, action replace: action.payload, } + case 'SET_REPLACE_ENABLED': + return { + ...state, + replaceEnabled: action.payload, + } + case 'SET_INCLUDE': return { ...state, diff --git a/libs/remix-ui/search/src/lib/search.css b/libs/remix-ui/search/src/lib/search.css index 895ce83e7f..8774dc310c 100644 --- a/libs/remix-ui/search/src/lib/search.css +++ b/libs/remix-ui/search/src/lib/search.css @@ -103,4 +103,21 @@ .search_plugin_search_tab .search_plugin_result_count_number { font-size: x-small; +} + +.search_plugin_find_container { + display: flex; + flex-direction: row; +} + +.search_plugin_find_container_internal { + display: flex; + flex-direction: column; + flex-grow: 1; +} + +.search_plugin_find_container_arrow { + display: flex !important; + align-items: center; + cursor: pointer !important; } \ 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 af76487c51..05f2200b1d 100644 --- a/libs/remix-ui/search/src/lib/types/index.ts +++ b/libs/remix-ui/search/src/lib/types/index.ts @@ -39,6 +39,7 @@ export interface SearchState { find: string, searchResults: SearchResult[], replace: string, + replaceEnabled: boolean, include: string, exclude: string, casesensitive: boolean, @@ -48,6 +49,8 @@ export interface SearchState { timeStamp: number, count: number, maxResults: number + maxFiles: number, + maxLines: number } export const SearchingInitialState: SearchState = { @@ -55,6 +58,7 @@ export const SearchingInitialState: SearchState = { replace: '', include: '', exclude: '', + replaceEnabled: false, searchResults: [], casesensitive: false, matchWord: false, @@ -62,5 +66,7 @@ export const SearchingInitialState: SearchState = { replaceWithOutConfirmation: false, timeStamp: 0, count: 0, - maxResults: 500 + maxResults: 1500, + maxFiles: 100, + maxLines: 200 } \ No newline at end of file From d511eb41827405b5f0b14efa247905e9450cb521 Mon Sep 17 00:00:00 2001 From: filip mertens Date: Fri, 11 Mar 2022 15:47:32 +0100 Subject: [PATCH 10/18] fix whitespace --- libs/remix-ui/search/src/lib/search.css | 1 + 1 file changed, 1 insertion(+) diff --git a/libs/remix-ui/search/src/lib/search.css b/libs/remix-ui/search/src/lib/search.css index 8774dc310c..ccf55adff2 100644 --- a/libs/remix-ui/search/src/lib/search.css +++ b/libs/remix-ui/search/src/lib/search.css @@ -64,6 +64,7 @@ .search_plugin_search_tab mark { padding: 0; + white-space: pre; } .search_plugin_search_tab .search_plugin_search_line_container .search_plugin_search_control { From cd700231a94870b2845c91e656f67e9cb165c0e0 Mon Sep 17 00:00:00 2001 From: bunsenstraat Date: Tue, 15 Mar 2022 12:41:08 +0100 Subject: [PATCH 11/18] merge --- .../search/src/lib/components/Exclude.tsx | 2 +- .../search/src/lib/components/Include.tsx | 2 +- .../search/src/lib/components/Replace.tsx | 2 +- .../search/src/lib/components/Search.tsx | 2 + .../search/src/lib/components/Undo.tsx | 29 +++++ .../src/lib/components/results/ResultItem.tsx | 26 +++- .../src/lib/components/results/Results.tsx | 4 +- .../lib/components/results/SearchHelper.ts | 3 + .../search/src/lib/context/context.tsx | 114 +++++++++++++++--- .../search/src/lib/reducers/Reducer.ts | 37 +++++- libs/remix-ui/search/src/lib/types/index.ts | 20 ++- 11 files changed, 207 insertions(+), 34 deletions(-) create mode 100644 libs/remix-ui/search/src/lib/components/Undo.tsx diff --git a/libs/remix-ui/search/src/lib/components/Exclude.tsx b/libs/remix-ui/search/src/lib/components/Exclude.tsx index c334116a6c..9047e26ce7 100644 --- a/libs/remix-ui/search/src/lib/components/Exclude.tsx +++ b/libs/remix-ui/search/src/lib/components/Exclude.tsx @@ -18,7 +18,7 @@ export const Exclude = props => { return ( <>
- + { return ( <>
- + { return ( <>
- + { @@ -21,6 +22,7 @@ return ( +
diff --git a/libs/remix-ui/search/src/lib/components/Undo.tsx b/libs/remix-ui/search/src/lib/components/Undo.tsx new file mode 100644 index 0000000000..e44cd23c88 --- /dev/null +++ b/libs/remix-ui/search/src/lib/components/Undo.tsx @@ -0,0 +1,29 @@ +import { useDialogDispatchers } from "@remix-ui/app" +import React from "react" +import { useContext } from "react" +import { SearchContext } from "../context/context" + +export const Undo = () => { + const { + state, + undoReplace + } = useContext(SearchContext) + const { alert } = useDialogDispatchers() + + + const undo = async () => { + try{ + await undoReplace(state.undoBuffer[0]) + }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 0728d220a1..c0d1ee1ff6 100644 --- a/libs/remix-ui/search/src/lib/components/results/ResultItem.tsx +++ b/libs/remix-ui/search/src/lib/components/results/ResultItem.tsx @@ -1,3 +1,4 @@ +import { useDialogDispatchers } from '@remix-ui/app' import React, { useContext, useEffect, useRef, useState } from 'react' import { SearchContext } from '../../context/context' import { SearchResult, SearchResultLine } from '../../types' @@ -9,7 +10,7 @@ interface ResultItemProps { } export const ResultItem = (props: ResultItemProps) => { - const { state, findText, disableForceReload, updateCount } = useContext( + const { state, findText, disableForceReload, updateCount, replaceAllInFile } = useContext( SearchContext ) const [loading, setLoading] = useState(false) @@ -17,7 +18,7 @@ export const ResultItem = (props: ResultItemProps) => { const [toggleExpander, setToggleExpander] = useState(false) const reloadTimeOut = useRef(null) const subscribed = useRef(true) - + const { modal } = useDialogDispatchers() useEffect(() => { reload() @@ -41,10 +42,28 @@ export const ResultItem = (props: ResultItemProps) => { useEffect(() => { subscribed.current = true return () => { + updateCount(0, props.file.filename) subscribed.current = false } }, []) + const confirmReplace = async () => { + setLoading(true) + try { + await replaceAllInFile(props.file) + } catch (e) { + } + setLoading(false) + } + + const replace = async () => { + if(state.replaceWithOutConfirmation){ + confirmReplace() + }else{ + modal({ id: 'confirmreplace', title: 'Replace', message: `Are you sure you want to replace '${state.find}' by '${state.replace}' in ${props.file.filename}?`, okLabel: 'Yes', okFn: confirmReplace, cancelLabel: 'No', cancelFn: ()=>{}, data: null }) + } + } + const reload = () => { findText(props.file.filename).then(res => { if (subscribed.current) { @@ -85,7 +104,8 @@ export const ResultItem = (props: ResultItemProps) => { {loading ?
Loading...
: null} {!toggleExpander && !loading ? (
- {lines.map((line, index) => ( + {state.replaceEnabled?
replace()} className='btn btn-primary btn-block mb-2 btn-sm'>Replace all
:null} + {lines.map((line, index) => ( index < state.maxLines ? { const { state } = useContext(SearchContext) return (
- {state.find ?
{state.count} results
: null} - {state.find && state.count >= state.maxResults?
The result set only contains a subset of all matches

Please narrow down your search.
: null} + {state.find ?
showing {state.count} results {state.fileCount} in files
: null} + {state.find && state.clipped?
The result set only shows a subset of all matches

Please narrow down your search.
: null} {state.searchResults && state.searchResults.map((result, index) => { return index : null diff --git a/libs/remix-ui/search/src/lib/components/results/SearchHelper.ts b/libs/remix-ui/search/src/lib/components/results/SearchHelper.ts index 88ae6b1de1..fb6b096df2 100644 --- a/libs/remix-ui/search/src/lib/components/results/SearchHelper.ts +++ b/libs/remix-ui/search/src/lib/components/results/SearchHelper.ts @@ -92,6 +92,9 @@ function getEOL(text) { return u > w ? '\n' : '\r\n'; } +export const replaceAllInFile = (string: string, re:RegExp, newText: string) => { + return string.replace(re, newText) +} export const replaceTextInLine = (str: string, searchResultLine: SearchResultLineLine, newText: string) => { return str diff --git a/libs/remix-ui/search/src/lib/context/context.tsx b/libs/remix-ui/search/src/lib/context/context.tsx index 267fa2a92e..9d131ba620 100644 --- a/libs/remix-ui/search/src/lib/context/context.tsx +++ b/libs/remix-ui/search/src/lib/context/context.tsx @@ -3,6 +3,7 @@ import { createContext, useReducer } from 'react' import { findLinesInStringWithMatch, getDirectory, + replaceAllInFile, replaceTextInLine } from '../components/results/SearchHelper' import { SearchReducer } from '../reducers/Reducer' @@ -11,7 +12,8 @@ import { SearchResult, SearchResultLine, SearchResultLineLine, - SearchingInitialState + SearchingInitialState, + undoBufferRecord } from '../types' import { filePathFilter } from '@jsdevtools/file-path-filter' import { escapeRegExp } from 'lodash' @@ -29,7 +31,7 @@ export interface SearchingStateInterface { setSearchResults: (value: SearchResult[]) => void findText: (path: string) => Promise hightLightInPath: (result: SearchResult, line: SearchResultLineLine) => void - replaceText: (result: SearchResult, line: SearchResultLineLine) => void + replaceText: (result: SearchResult, line: SearchResultLineLine) => Promise reloadFile: (file: string) => void toggleCaseSensitive: () => void toggleMatchWholeWord: () => void @@ -37,6 +39,9 @@ export interface SearchingStateInterface { setReplaceWithoutConfirmation: (value: boolean) => void disableForceReload: (file: string) => void updateCount: (count: number, file: string) => void + replaceAllInFile: (result: SearchResult) => Promise + undoReplace: (buffer: undoBufferRecord) => Promise + clearUndo: () => void } export const SearchContext = createContext(null) @@ -145,7 +150,7 @@ export const SearchProvider = ({ updateCount: (count: number, file: string) => { dispatch({ type: 'UPDATE_COUNT', - payload: {count, file} + payload: { count, file } }) }, findText: async (path: string) => { @@ -153,15 +158,9 @@ export const SearchProvider = ({ try { if (state.find.length < 1) return const text = await plugin.call('fileManager', 'readFile', path) - let flags = 'g' - let find = state.find - if (!state.casesensitive) flags += 'i' - if (!state.useRegExp) find = escapeRegExp(find) - if (state.matchWord) find = `\\b${find}\\b` - const re = new RegExp(find, flags) - const result: SearchResultLine[] = findLinesInStringWithMatch(text, re) + const result: SearchResultLine[] = findLinesInStringWithMatch(text, createRegExFromFind()) return result - } catch (e) {} + } catch (e) { } }, hightLightInPath: async ( result: SearchResult, @@ -180,26 +179,81 @@ export const SearchProvider = ({ 'readFile', result.path ) - + const replaced = replaceTextInLine(content, line, state.replace) await plugin.call( 'fileManager', 'setFile', result.path, - replaceTextInLine(content, line, state.replace) + replaced ) + setUndoState(content, replaced, result.path) } catch (e) { throw new Error(e) } + }, + replaceAllInFile: async (result: SearchResult) => { + await plugin.call('editor', 'discardHighlight') + const content = await plugin.call( + 'fileManager', + 'readFile', + result.path + ) + const replaced = replaceAllInFile(content, createRegExFromFind(), state.replace) + await plugin.call( + 'fileManager', + 'setFile', + result.path, + replaced + ) + await plugin.call( + 'fileManager', + 'open', + result.path + ) + setUndoState(content, replaced, result.path) + }, + undoReplace: async (buffer: undoBufferRecord) => { + const content = await plugin.call( + 'fileManager', + 'readFile', + buffer.path + ) + if (buffer.newContent !== content) { + value.clearUndo() + throw new Error('Can not undo replace, file has been changed.') + + } + await plugin.call( + 'fileManager', + 'setFile', + buffer.path, + buffer.oldContent + ) + await plugin.call( + 'fileManager', + 'open', + buffer.path + ) + value.clearUndo() + }, + clearUndo: () => { + dispatch ({ + type: 'CLEAR_UNDO', + payload: undefined + }) } } + + const reloadStateForFile = async (file: string) => { - await value.reloadFile(file) + await value.reloadFile(file) } useEffect(() => { plugin.on('filePanel', 'setWorkspace', () => { value.setSearchResults(null) + value.clearUndo() }) plugin.on('fileManager', 'fileSaved', async file => { await reloadStateForFile(file) @@ -215,22 +269,46 @@ export const SearchProvider = ({ const results = [] paths.split(',').forEach(path => { path = path.trim() - if(path.startsWith('*.')) path = path.replace(/(\*\.)/g, '**/*.') - if(path.endsWith('/*') && !path.endsWith('/**/*')) path = path.replace(/(\*)/g, '**/*.*') + if (path.startsWith('*.')) path = path.replace(/(\*\.)/g, '**/*.') + if (path.endsWith('/*') && !path.endsWith('/**/*')) path = path.replace(/(\*)/g, '**/*.*') results.push(path) }) return results } + const setUndoState = async (oldContent: string, newContent: string, path: string) => { + const workspace = await plugin.call('filePanel', 'getCurrentWorkspace') + const undo = { + oldContent, + newContent, + path, + workspace + } + dispatch({ + type: 'SET_UNDO', + payload: undo + }) + } + + const createRegExFromFind = () => { + let flags = 'g' + let find = state.find + if (!state.casesensitive) flags += 'i' + if (!state.useRegExp) find = escapeRegExp(find) + if (state.matchWord) find = `\\b${find}\\b` + const re = new RegExp(find, flags) + return re + } + useEffect(() => { if (state.find) { (async () => { const files = await getDirectory('/', plugin) const pathFilter: any = {} - if (state.include){ + if (state.include) { pathFilter.include = setGlobalExpression(state.include) } - if (state.exclude){ + if (state.exclude) { pathFilter.exclude = setGlobalExpression(state.exclude) } const filteredFiles = files.filter(filePathFilter(pathFilter)).map(file => { diff --git a/libs/remix-ui/search/src/lib/reducers/Reducer.ts b/libs/remix-ui/search/src/lib/reducers/Reducer.ts index 47b9cef21f..b40d8ab4c6 100644 --- a/libs/remix-ui/search/src/lib/reducers/Reducer.ts +++ b/libs/remix-ui/search/src/lib/reducers/Reducer.ts @@ -1,4 +1,4 @@ -import { Action, SearchingInitialState, SearchState } from "../types" +import { Action, SearchingInitialState, SearchState, undoBufferRecord } from "../types" export const SearchReducer = (state: SearchState = SearchingInitialState, action: Action) => { switch (action.type) { @@ -41,21 +41,50 @@ export const SearchReducer = (state: SearchState = SearchingInitialState, action searchResults: action.payload, count: 0 } + case 'SET_UNDO': { + const undoState = { + newContent : action.payload.newContent, + oldContent: action.payload.oldContent, + path: action.payload.path, + workspace: action.payload.workspace, + timeStamp: Date.now() + } + state.undoBuffer = [undoState] + return { + ...state, + } + } + case 'CLEAR_UNDO': { + state.undoBuffer = [] + return { + ...state, + } + } case 'UPDATE_COUNT': if (state.searchResults) { const findFile = state.searchResults.find(file => file.filename === action.payload.file) let count = 0 + let fileCount = 0 + let clipped = false if (findFile) { findFile.count = action.payload.count } state.searchResults.forEach(file => { - if (file.count) { - count += file.count + if (file.count) { + if(file.count > state.maxLines) { + clipped = true + count += state.maxLines + }else{ + count += file.count + } + fileCount++ } }) return { ...state, - count: count + count: count, + fileCount, + clipped } } else { return state diff --git a/libs/remix-ui/search/src/lib/types/index.ts b/libs/remix-ui/search/src/lib/types/index.ts index 05f2200b1d..aa11435099 100644 --- a/libs/remix-ui/search/src/lib/types/index.ts +++ b/libs/remix-ui/search/src/lib/types/index.ts @@ -35,6 +35,14 @@ export interface SearchResult { count: number } +export interface undoBufferRecord { + workspace: string, + path: string, + newContent: string, + timeStamp: number, + oldContent: string +} + export interface SearchState { find: string, searchResults: SearchResult[], @@ -48,9 +56,11 @@ export interface SearchState { useRegExp: boolean, timeStamp: number, count: number, - maxResults: number + fileCount: number, maxFiles: number, maxLines: number + clipped: boolean, + undoBuffer: undoBufferRecord[], } export const SearchingInitialState: SearchState = { @@ -66,7 +76,9 @@ export const SearchingInitialState: SearchState = { replaceWithOutConfirmation: false, timeStamp: 0, count: 0, - maxResults: 1500, - maxFiles: 100, - maxLines: 200 + fileCount: 0, + maxFiles: 5000, + maxLines: 5000, + clipped: false, + undoBuffer: null } \ No newline at end of file From 94f289df31475b56a4f126f53fe62c47efbad51e Mon Sep 17 00:00:00 2001 From: bunsenstraat Date: Wed, 16 Mar 2022 09:54:43 +0100 Subject: [PATCH 12/18] layout --- libs/remix-ui/search/src/lib/components/Replace.tsx | 2 +- libs/remix-ui/search/src/lib/components/Undo.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/remix-ui/search/src/lib/components/Replace.tsx b/libs/remix-ui/search/src/lib/components/Replace.tsx index 33d414d4e7..a8bd50177f 100644 --- a/libs/remix-ui/search/src/lib/components/Replace.tsx +++ b/libs/remix-ui/search/src/lib/components/Replace.tsx @@ -11,7 +11,7 @@ export const Replace = props => { return ( <> -
+
{ return (<> {state.undoBuffer && state.undoBuffer.length > 0 ? - : null} From 1f255d2008fa3e8926bdf9338222d63d2d4cb7d2 Mon Sep 17 00:00:00 2001 From: bunsenstraat Date: Wed, 16 Mar 2022 09:54:50 +0100 Subject: [PATCH 13/18] layout --- libs/remix-ui/search/src/lib/components/Exclude.tsx | 2 +- libs/remix-ui/search/src/lib/components/FindContainer.tsx | 4 +++- libs/remix-ui/search/src/lib/components/Search.tsx | 3 +-- .../remix-ui/search/src/lib/components/results/ResultItem.tsx | 4 ++-- libs/remix-ui/search/src/lib/search.css | 2 +- 5 files changed, 8 insertions(+), 7 deletions(-) diff --git a/libs/remix-ui/search/src/lib/components/Exclude.tsx b/libs/remix-ui/search/src/lib/components/Exclude.tsx index 9047e26ce7..5385783324 100644 --- a/libs/remix-ui/search/src/lib/components/Exclude.tsx +++ b/libs/remix-ui/search/src/lib/components/Exclude.tsx @@ -18,7 +18,7 @@ export const Exclude = props => { return ( <>
- + { @@ -26,7 +27,8 @@ export const FindContainer = props => { >
- {expanded ? : null} + {expanded ? + <> : null}
) diff --git a/libs/remix-ui/search/src/lib/components/Search.tsx b/libs/remix-ui/search/src/lib/components/Search.tsx index a8f32ce4d5..7f6d62aa5e 100644 --- a/libs/remix-ui/search/src/lib/components/Search.tsx +++ b/libs/remix-ui/search/src/lib/components/Search.tsx @@ -16,12 +16,11 @@ const plugin = props.plugin return ( <> -
+
- 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 c0d1ee1ff6..b0ac872251 100644 --- a/libs/remix-ui/search/src/lib/components/results/ResultItem.tsx +++ b/libs/remix-ui/search/src/lib/components/results/ResultItem.tsx @@ -103,8 +103,8 @@ export const ResultItem = (props: ResultItemProps) => {
{loading ?
Loading...
: null} {!toggleExpander && !loading ? ( -
- {state.replaceEnabled?
replace()} className='btn btn-primary btn-block mb-2 btn-sm'>Replace all
:null} +
+ {state.replaceEnabled?
replace()} className='btn btn-secondary btn-block mb-2 btn-sm'>Replace all
:null} {lines.map((line, index) => ( index < state.maxLines ? Date: Wed, 16 Mar 2022 16:00:38 +0100 Subject: [PATCH 14/18] multi undo --- apps/remix-ide-e2e/src/tests/search.test.ts | 131 ++++++++++++++++-- apps/remix-ide/src/app/tabs/search.tsx | 2 +- apps/remix-ide/src/assets/img/Search_Icon.svg | 77 ---------- .../remix-ide/src/assets/img/search_icon.webp | Bin 0 -> 44302 bytes .../src/lib/components/OverWriteCheck.tsx | 2 +- .../search/src/lib/components/Undo.tsx | 10 +- .../src/lib/components/results/ResultItem.tsx | 6 +- .../search/src/lib/context/context.tsx | 51 ++++++- .../search/src/lib/reducers/Reducer.ts | 24 +++- libs/remix-ui/search/src/lib/search.css | 12 ++ libs/remix-ui/search/src/lib/types/index.ts | 13 +- 11 files changed, 223 insertions(+), 105 deletions(-) delete mode 100644 apps/remix-ide/src/assets/img/Search_Icon.svg create mode 100644 apps/remix-ide/src/assets/img/search_icon.webp 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 0000000000000000000000000000000000000000..93b26959314d26466576dde0c4684a399a466eeb GIT binary patch literal 44302 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®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 z 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 Lfya6nfWr7cd%yp$ literal 0 HcmV?d00001 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 From 58ae2ea6743052dc106008120a53a80bc63993d7 Mon Sep 17 00:00:00 2001 From: filip mertens Date: Wed, 16 Mar 2022 16:02:27 +0100 Subject: [PATCH 15/18] rm grouping --- apps/remix-ide-e2e/src/tests/search.test.ts | 26 ++++++++++----------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/apps/remix-ide-e2e/src/tests/search.test.ts b/apps/remix-ide-e2e/src/tests/search.test.ts index 8e7c7b8ba9..ce754fc425 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 #group3': function (browser: NightwatchBrowser) { + 'Should find text': 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 #group3': function (browser: NightwatchBrowser) { + 'Should find regex': 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 #group3': function (browser: NightwatchBrowser) { + 'Should find matchcase': 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 #group3': function (browser: NightwatchBrowser) { + 'Should find matchword': 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 #group3': function (browser: NightwatchBrowser) { + 'Should replace text': 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 #group3': function (browser: NightwatchBrowser) { + 'Should replace text without confirmation': 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,7 @@ module.exports = { browser.assert.ok(content.includes('replacing2 deployer for a constructor'), 'should replace text ok') }) }, - 'Should replace all & undo #group3': function (browser: NightwatchBrowser) { + 'Should replace all & undo': function (browser: NightwatchBrowser) { browser .clearValue('*[id="search_input"]') .setValue('*[id="search_input"]', 'storage') @@ -111,7 +111,7 @@ module.exports = { browser.assert.ok(content.includes('title Storage'), 'should undo text ok') }) }, - 'Should replace all & undo & switch between files #group3': function (browser: NightwatchBrowser) { + 'Should replace all & undo & switch between files': function (browser: NightwatchBrowser) { browser.waitForElementVisible('*[id="search_input"]') .clearValue('*[id="search_input"]') .setValue('*[id="search_input"]', 'storage') @@ -147,7 +147,7 @@ module.exports = { browser.assert.ok(content.includes("Storage' contract"), 'should replace text ok') }) }, - 'Should hide button when edited content is the same #group3': function (browser: NightwatchBrowser) { + 'Should hide button when edited content is the same': function (browser: NightwatchBrowser) { browser.waitForElementVisible('*[data-id="remixIdeSidePanel"]') .addFile('test.sol', { content: '123'}) .click('*[plugin="search"]').waitForElementVisible('*[id="search_input"]') @@ -168,7 +168,7 @@ module.exports = { ).pause(1000) .waitForElementNotPresent('*[data-id="undo-replace-test.sol"]') }, - 'Should disable/enable button when edited content changed #group3': function (browser: NightwatchBrowser) { + 'Should disable/enable button when edited content changed': function (browser: NightwatchBrowser) { browser .waitForElementVisible('*[id="search_input"]') .clearValue('*[id="search_input"]') @@ -205,7 +205,7 @@ module.exports = { }) .waitForElementNotPresent('*[data-id="undo-replace-test.sol"]') }, - 'Should find text with include #group3': function (browser: NightwatchBrowser) { + 'Should find text with include': function (browser: NightwatchBrowser) { browser .clearValue('*[id="search_input"]') .setValue('*[id="search_input"]', 'contract').pause(1000) @@ -214,7 +214,7 @@ module.exports = { Array.isArray(res.value) && browser.assert.equal(res.value.length, 4) }) }, - 'Should find text with exclude #group3': function (browser: NightwatchBrowser) { + 'Should find text with exclude': function (browser: NightwatchBrowser) { browser .clearValue('*[id="search_include"]').pause(2000) .setValue('*[id="search_include"]', '**').pause(2000) @@ -226,7 +226,7 @@ module.exports = { Array.isArray(res.value) && browser.assert.equal(res.value.length, 22) }) }, - 'should clear search #group3': function (browser: NightwatchBrowser) { + 'should clear search': function (browser: NightwatchBrowser) { browser .waitForElementVisible('*[id="search_input"]') .setValue('*[id="search_input"]', 'nodata').pause(1000) From c945fdbcfb9601f67a980c44454dc25826428118 Mon Sep 17 00:00:00 2001 From: filip mertens Date: Wed, 16 Mar 2022 16:04:30 +0100 Subject: [PATCH 16/18] space --- libs/remix-ui/search/src/lib/components/Undo.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/remix-ui/search/src/lib/components/Undo.tsx b/libs/remix-ui/search/src/lib/components/Undo.tsx index e920a3671d..03cbbf4550 100644 --- a/libs/remix-ui/search/src/lib/components/Undo.tsx +++ b/libs/remix-ui/search/src/lib/components/Undo.tsx @@ -26,4 +26,4 @@ export const Undo = () => { Undo changes to {path.basename(state.undoBuffer[`${state.workspace}/${state.currentFile}`].path)} : null} ) -} \ No newline at end of file +} From 9e1938fdd8cafad37483379a8cc58d8a74ecda6a Mon Sep 17 00:00:00 2001 From: filip mertens Date: Wed, 16 Mar 2022 20:06:43 +0100 Subject: [PATCH 17/18] disable --- apps/remix-ide-e2e/src/tests/search.test.ts | 2 +- apps/remix-ide/src/app.js | 7 +++---- apps/remix-ide/src/app/files/dgitProvider.js | 1 - apps/remix-ide/src/remixAppManager.js | 2 +- 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/apps/remix-ide-e2e/src/tests/search.test.ts b/apps/remix-ide-e2e/src/tests/search.test.ts index ce754fc425..f0cc787924 100644 --- a/apps/remix-ide-e2e/src/tests/search.test.ts +++ b/apps/remix-ide-e2e/src/tests/search.test.ts @@ -4,7 +4,7 @@ import { NightwatchBrowser } from 'nightwatch' import init from '../helpers/init' module.exports = { - + '@disabled': true, before: function (browser: NightwatchBrowser, done: VoidFunction) { init(browser, done, 'http://127.0.0.1:8080', true) }, diff --git a/apps/remix-ide/src/app.js b/apps/remix-ide/src/app.js index 732ef24c4a..8fef85bf67 100644 --- a/apps/remix-ide/src/app.js +++ b/apps/remix-ide/src/app.js @@ -30,7 +30,7 @@ const isElectron = require('is-electron') const remixLib = require('@remix-project/remix-lib') import { QueryParams } from '@remix-project/remix-lib' -import { SearchPlugin } from './app/tabs/search' + const Storage = remixLib.Storage const RemixDProvider = require('./app/files/remixDProvider') const Config = require('./config') @@ -149,7 +149,7 @@ class AppComponent { const storagePlugin = new StoragePlugin() //----- search - const search = new SearchPlugin() + // const search = new SearchPlugin() // ----------------- import content service ------------------------ const contentImport = new CompilerImports() @@ -226,7 +226,6 @@ class AppComponent { storagePlugin, hardhatProvider, this.walkthroughService, - search ]) // LAYOUT & SYSTEM VIEWS @@ -337,7 +336,7 @@ class AppComponent { await this.appManager.activatePlugin(['settings', 'config']) await this.appManager.activatePlugin(['hiddenPanel', 'pluginManager', 'contextualListener', 'terminal', 'blockchain', 'fetchAndCompile', 'contentImport', 'gistHandler']) await this.appManager.activatePlugin(['settings']) - await this.appManager.activatePlugin(['walkthrough','storage', 'search']) + await this.appManager.activatePlugin(['walkthrough','storage']) this.appManager.on( 'filePanel', diff --git a/apps/remix-ide/src/app/files/dgitProvider.js b/apps/remix-ide/src/app/files/dgitProvider.js index 9a2fcc8e80..7bd76908ba 100644 --- a/apps/remix-ide/src/app/files/dgitProvider.js +++ b/apps/remix-ide/src/app/files/dgitProvider.js @@ -129,7 +129,6 @@ class DGitProvider extends Plugin { try { remotes = await git.listRemotes({ ...await this.getGitConfig() }) } catch (e) { - console.log(e) } return remotes } diff --git a/apps/remix-ide/src/remixAppManager.js b/apps/remix-ide/src/remixAppManager.js index 7f8269201c..6a1790e3d0 100644 --- a/apps/remix-ide/src/remixAppManager.js +++ b/apps/remix-ide/src/remixAppManager.js @@ -7,7 +7,7 @@ const _paq = window._paq = window._paq || [] const requiredModules = [ // services + layout views + system views 'manager', 'config', 'compilerArtefacts', 'compilerMetadata', 'contextualListener', 'editor', 'offsetToLineColumnConverter', 'network', 'theme', 'fileManager', 'contentImport', 'blockchain', 'web3Provider', 'scriptRunner', 'fetchAndCompile', 'mainPanel', 'hiddenPanel', 'sidePanel', 'menuicons', - 'filePanel', 'terminal', 'settings', 'pluginManager', 'tabs', 'udapp', 'dGitProvider', 'solidity-logic', 'gistHandler', 'layout', 'notification', 'permissionhandler', 'walkthrough', 'storage', 'search'] + 'filePanel', 'terminal', 'settings', 'pluginManager', 'tabs', 'udapp', 'dGitProvider', 'solidity-logic', 'gistHandler', 'layout', 'notification', 'permissionhandler', 'walkthrough', 'storage'] const dependentModules = ['git', 'hardhat', 'slither'] // module which shouldn't be manually activated (e.g git is activated by remixd) From 387ff55a96324fa232de36df9252902c45d4b92f Mon Sep 17 00:00:00 2001 From: filip mertens Date: Wed, 16 Mar 2022 20:14:23 +0100 Subject: [PATCH 18/18] empty block --- apps/remix-ide/src/app/files/dgitProvider.js | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/remix-ide/src/app/files/dgitProvider.js b/apps/remix-ide/src/app/files/dgitProvider.js index 7bd76908ba..249640a0f3 100644 --- a/apps/remix-ide/src/app/files/dgitProvider.js +++ b/apps/remix-ide/src/app/files/dgitProvider.js @@ -129,6 +129,7 @@ class DGitProvider extends Plugin { try { remotes = await git.listRemotes({ ...await this.getGitConfig() }) } catch (e) { + // do nothing } return remotes }