Merge branch 'master' into clear_storage

pull/2191/head
Rob 3 years ago committed by GitHub
commit c414bf31d2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 117
      apps/remix-ide-e2e/src/tests/search.test.ts
  2. 7
      apps/remix-ide/src/app.js
  3. 16
      apps/remix-ide/src/app/editor/editor.js
  4. 2
      apps/remix-ide/src/app/files/dgitProvider.js
  5. 4
      apps/remix-ide/src/app/tabs/search.tsx
  6. 77
      apps/remix-ide/src/assets/img/Search_Icon.svg
  7. BIN
      apps/remix-ide/src/assets/img/search_icon.webp
  8. 2
      apps/remix-ide/src/remixAppManager.js
  9. 37
      libs/remix-ui/editor/src/lib/actions/editor.ts
  10. 4
      libs/remix-ui/search/src/lib/components/Exclude.tsx
  11. 1
      libs/remix-ui/search/src/lib/components/Find.tsx
  12. 35
      libs/remix-ui/search/src/lib/components/FindContainer.tsx
  13. 4
      libs/remix-ui/search/src/lib/components/Include.tsx
  14. 36
      libs/remix-ui/search/src/lib/components/OverWriteCheck.tsx
  15. 4
      libs/remix-ui/search/src/lib/components/Replace.tsx
  16. 9
      libs/remix-ui/search/src/lib/components/Search.tsx
  17. 29
      libs/remix-ui/search/src/lib/components/Undo.tsx
  18. 35
      libs/remix-ui/search/src/lib/components/results/ResultItem.tsx
  19. 7
      libs/remix-ui/search/src/lib/components/results/ResultSummary.tsx
  20. 10
      libs/remix-ui/search/src/lib/components/results/Results.tsx
  21. 3
      libs/remix-ui/search/src/lib/components/results/SearchHelper.ts
  22. 167
      libs/remix-ui/search/src/lib/context/context.tsx
  23. 63
      libs/remix-ui/search/src/lib/reducers/Reducer.ts
  24. 32
      libs/remix-ui/search/src/lib/search.css
  25. 27
      libs/remix-ui/search/src/lib/types/index.ts

@ -4,7 +4,7 @@ import { NightwatchBrowser } from 'nightwatch'
import init from '../helpers/init' import init from '../helpers/init'
module.exports = { module.exports = {
'@disabled': true,
before: function (browser: NightwatchBrowser, done: VoidFunction) { before: function (browser: NightwatchBrowser, done: VoidFunction) {
init(browser, done, 'http://127.0.0.1:8080', true) init(browser, done, 'http://127.0.0.1:8080', true)
}, },
@ -67,6 +67,8 @@ module.exports = {
}, },
'Should replace text': function (browser: NightwatchBrowser) { 'Should replace text': function (browser: NightwatchBrowser) {
browser browser
.waitForElementVisible('*[data-id="toggle_replace"]').click('*[data-id="toggle_replace"]')
.waitForElementVisible('*[id="search_replace"]')
.setValue('*[id="search_replace"]', 'replacing').pause(1000) .setValue('*[id="search_replace"]', 'replacing').pause(1000)
.waitForElementVisible('*[data-id="contracts/2_Owner.sol-30-71"]') .waitForElementVisible('*[data-id="contracts/2_Owner.sol-30-71"]')
.moveToElement('*[data-id="contracts/2_Owner.sol-30-71"]', 10, 10) .moveToElement('*[data-id="contracts/2_Owner.sol-30-71"]', 10, 10)
@ -90,6 +92,119 @@ module.exports = {
browser.assert.ok(content.includes('replacing2 deployer for a constructor'), 'should replace text ok') browser.assert.ok(content.includes('replacing2 deployer for a constructor'), 'should replace text ok')
}) })
}, },
'Should replace all & undo': 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': 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': 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': 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': function (browser: NightwatchBrowser) { 'Should find text with include': function (browser: NightwatchBrowser) {
browser browser
.clearValue('*[id="search_input"]') .clearValue('*[id="search_input"]')

@ -30,7 +30,7 @@ const isElectron = require('is-electron')
const remixLib = require('@remix-project/remix-lib') const remixLib = require('@remix-project/remix-lib')
import { QueryParams } from '@remix-project/remix-lib' import { QueryParams } from '@remix-project/remix-lib'
import { SearchPlugin } from './app/tabs/search'
const Storage = remixLib.Storage const Storage = remixLib.Storage
const RemixDProvider = require('./app/files/remixDProvider') const RemixDProvider = require('./app/files/remixDProvider')
const Config = require('./config') const Config = require('./config')
@ -149,7 +149,7 @@ class AppComponent {
const storagePlugin = new StoragePlugin() const storagePlugin = new StoragePlugin()
//----- search //----- search
const search = new SearchPlugin() // const search = new SearchPlugin()
// ----------------- import content service ------------------------ // ----------------- import content service ------------------------
const contentImport = new CompilerImports() const contentImport = new CompilerImports()
@ -226,7 +226,6 @@ class AppComponent {
storagePlugin, storagePlugin,
hardhatProvider, hardhatProvider,
this.walkthroughService, this.walkthroughService,
search
]) ])
// LAYOUT & SYSTEM VIEWS // LAYOUT & SYSTEM VIEWS
@ -337,7 +336,7 @@ class AppComponent {
await this.appManager.activatePlugin(['settings', 'config']) await this.appManager.activatePlugin(['settings', 'config'])
await this.appManager.activatePlugin(['hiddenPanel', 'pluginManager', 'contextualListener', 'terminal', 'blockchain', 'fetchAndCompile', 'contentImport', 'gistHandler']) await this.appManager.activatePlugin(['hiddenPanel', 'pluginManager', 'contextualListener', 'terminal', 'blockchain', 'fetchAndCompile', 'contentImport', 'gistHandler'])
await this.appManager.activatePlugin(['settings']) await this.appManager.activatePlugin(['settings'])
await this.appManager.activatePlugin(['walkthrough','storage', 'search']) await this.appManager.activatePlugin(['walkthrough','storage'])
this.appManager.on( this.appManager.on(
'filePanel', 'filePanel',

@ -12,7 +12,7 @@ const profile = {
name: 'editor', name: 'editor',
description: 'service - editor', description: 'service - editor',
version: packageJson.version, version: packageJson.version,
methods: ['highlight', 'discardHighlight', 'clearAnnotations', 'addAnnotation', 'gotoLine', 'getCursorPosition'] methods: ['highlight', 'discardHighlight', 'clearAnnotations', 'addAnnotation', 'gotoLine', 'revealRange', 'getCursorPosition']
} }
class Editor extends Plugin { class Editor extends Plugin {
@ -390,6 +390,20 @@ class Editor extends Plugin {
this.emit('revealLine', line + 1, col) 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). * 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 * @param {number} line The line to scroll to

@ -129,7 +129,7 @@ class DGitProvider extends Plugin {
try { try {
remotes = await git.listRemotes({ ...await this.getGitConfig() }) remotes = await git.listRemotes({ ...await this.getGitConfig() })
} catch (e) { } catch (e) {
console.log(e) // do nothing
} }
return remotes return remotes
} }

@ -4,10 +4,10 @@ import React from 'react' // eslint-disable-line
import { SearchTab } from '@remix-ui/search' import { SearchTab } from '@remix-ui/search'
const profile = { const profile = {
name: 'search', name: 'search',
displayName: 'Search', displayName: 'Search in files',
methods: [''], methods: [''],
events: [], events: [],
icon: 'assets/img/Search_Icon.svg', icon: 'assets/img/search_icon.webp',
description: '', description: '',
kind: '', kind: '',
location: 'sidePanel', location: 'sidePanel',

@ -1,77 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="500"
height="500"
viewBox="0 0 500.00001 500.00001"
id="svg4162"
version="1.1"
inkscape:version="0.92.3 (2405546, 2018-03-11)"
sodipodi:docname="Search_Icon.svg">
<defs
id="defs4164" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.954"
inkscape:cx="250"
inkscape:cy="250"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
units="px"
inkscape:window-width="1366"
inkscape:window-height="706"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1" />
<metadata
id="metadata4167">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-552.36216)">
<g
id="g1400"
transform="translate(-4.3609793,-7.6704785)">
<path
inkscape:connector-curvature="0"
id="path4714"
d="M 232.83952,614.96702 A 154.04816,154.04794 0 0 0 78.79153,769.01382 154.04816,154.04794 0 0 0 232.83952,923.06184 154.04816,154.04794 0 0 0 386.88751,769.01382 154.04816,154.04794 0 0 0 232.83952,614.96702 Z m 0,26.77613 A 129.95832,127.2707 0 0 1 362.79832,769.01382 129.95832,127.2707 0 0 1 232.83952,896.28449 129.95832,127.2707 0 0 1 102.88194,769.01382 129.95832,127.2707 0 0 1 232.83952,641.74315 Z"
style="opacity:1;fill:#2b0000;fill-opacity:1;stroke:none;stroke-opacity:1" />
<rect
ry="18.08342"
rx="33.249443"
transform="matrix(0.65316768,0.7572133,-0.60689051,0.79478545,0,0)"
y="319.55432"
x="794.8775"
height="36.16684"
width="173.02675"
id="rect4721"
style="opacity:1;fill:#2b0000;fill-opacity:1;stroke:none;stroke-opacity:1" />
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

@ -7,7 +7,7 @@ const _paq = window._paq = window._paq || []
const requiredModules = [ // services + layout views + system views const requiredModules = [ // services + layout views + system views
'manager', 'config', 'compilerArtefacts', 'compilerMetadata', 'contextualListener', 'editor', 'offsetToLineColumnConverter', 'network', 'theme', 'manager', 'config', 'compilerArtefacts', 'compilerMetadata', 'contextualListener', 'editor', 'offsetToLineColumnConverter', 'network', 'theme',
'fileManager', 'contentImport', 'blockchain', 'web3Provider', 'scriptRunner', 'fetchAndCompile', 'mainPanel', 'hiddenPanel', 'sidePanel', 'menuicons', '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) const dependentModules = ['git', 'hardhat', 'slither'] // module which shouldn't be manually activated (e.g git is activated by remixd)

@ -1,3 +1,5 @@
import { IRange } from "monaco-editor";
export interface Action { export interface Action {
type: string; type: string;
payload: Record<string, any> payload: Record<string, any>
@ -49,6 +51,27 @@ export const reducerActions = (models = initialState, action: Action) => {
editor.setPosition({ column, lineNumber: line }) editor.setPosition({ column, lineNumber: line })
return models 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': { case 'FOCUS': {
if (!editor) return models if (!editor) return models
editor.focus() 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', () => { plugin.on('editor', 'focus', () => {
dispatch({ dispatch({
type: 'FOCUS', type: 'FOCUS',

@ -17,8 +17,8 @@ export const Exclude = props => {
return ( return (
<> <>
<div className="search_plugin_find-part"> <div className="search_plugin_find-part pl-3">
<label>exclude</label> <label className='mt-2'>Files to exclude</label>
<input <input
id='search_exclude' id='search_exclude'
placeholder="Exclude ie .git/**/*" placeholder="Exclude ie .git/**/*"

@ -18,7 +18,6 @@ export const Find = () => {
return ( return (
<> <>
<div className="search_plugin_find-part"> <div className="search_plugin_find-part">
<label>search</label>
<div className="search_plugin_search-input"> <div className="search_plugin_search-input">
<input <input
id='search_input' id='search_input'

@ -0,0 +1,35 @@
import React, { useContext, useEffect, useState } from 'react'
import { SearchContext } from '../context/context'
import { Find } from './Find'
import { OverWriteCheck } from './OverWriteCheck'
import { Replace } from './Replace'
export const FindContainer = props => {
const { setReplaceEnabled } = useContext(SearchContext)
const [expanded, setExpanded] = useState<boolean>(false)
const toggleExpand = () => setExpanded(!expanded)
useEffect(() => {
setReplaceEnabled(expanded)
}, [expanded])
return (
<div className="search_plugin_find_container">
<div
title="Toggle Replace"
data-id="toggle_replace"
className={`codicon codicon-find-${
expanded ? 'expanded' : 'collapsed'
} search_plugin_find_container_arrow`}
role="button"
onClick={toggleExpand}
aria-label="Toggle Replace"
aria-expanded="true"
aria-disabled="false"
></div>
<div className="search_plugin_find_container_internal">
<Find></Find>
{expanded ?
<><Replace></Replace><OverWriteCheck></OverWriteCheck></> : null}
</div>
</div>
)
}

@ -13,8 +13,8 @@ export const Include = props => {
return ( return (
<> <>
<div className="search_plugin_find-part"> <div className="search_plugin_find-part pl-3">
<label>include</label> <label className='mt-2'>Files to include</label>
<input <input
id='search_include' id='search_include'
placeholder="Include ie contracts/**/*.sol" placeholder="Include ie contracts/**/*.sol"

@ -2,7 +2,7 @@ import React, { useContext } from 'react'
import { SearchContext } from '../context/context' import { SearchContext } from '../context/context'
export const OverWriteCheck = props => { export const OverWriteCheck = props => {
const { setReplaceWithoutConfirmation } = useContext(SearchContext) const { setReplaceWithoutConfirmation, state } = useContext(SearchContext)
const change = e => { const change = e => {
setReplaceWithoutConfirmation(e.target.checked) setReplaceWithoutConfirmation(e.target.checked)
@ -10,23 +10,25 @@ export const OverWriteCheck = props => {
return ( return (
<> <>
<div className="search_plugin_find-part"> {state.replaceEnabled ? (
<div className="mb-2 remixui_nightlyBuilds custom-control custom-checkbox"> <div className="search_plugin_find-part">
<input <div className="mb-2 remixui_nightlyBuilds custom-control custom-checkbox">
className="mr-2 custom-control-input" <input
id="confirm_replace" className="mr-2 custom-control-input"
type="checkbox" id="confirm_replace"
onChange={change} type="checkbox"
/> onChange={change}
<label />
htmlFor='confirm_replace' <label
data-id="confirm_replace_label" htmlFor="confirm_replace"
className="form-check-label custom-control-label" data-id="confirm_replace_label"
> className="form-check-label custom-control-label"
replace without confirmation >
</label> replace without confirmation
</label>
</div>
</div> </div>
</div> ) : null}
</> </>
) )
} }

@ -11,8 +11,8 @@ export const Replace = props => {
return ( return (
<> <>
<div className="search_plugin_find-part"> <div className="search_plugin_find-part ">
<label>replace</label> <label className='d-none'>replace in files</label>
<input <input
id='search_replace' id='search_replace'
placeholder="Replace" placeholder="Replace"

@ -7,6 +7,8 @@ import { Include } from './Include'
import { Exclude } from './Exclude' import { Exclude } from './Exclude'
import { Replace } from './Replace' import { Replace } from './Replace'
import { OverWriteCheck } from './OverWriteCheck' import { OverWriteCheck } from './OverWriteCheck'
import { FindContainer } from './FindContainer'
import { Undo } from './Undo'
export const SearchTab = props => { export const SearchTab = props => {
@ -14,13 +16,12 @@ const plugin = props.plugin
return ( return (
<> <>
<div className="search_plugin_search_tab pl-2 pr-2"> <div className="search_plugin_search_tab px-2">
<SearchProvider plugin={plugin}> <SearchProvider plugin={plugin}>
<Find></Find> <FindContainer></FindContainer>
<Replace></Replace>
<Include></Include> <Include></Include>
<Exclude></Exclude> <Exclude></Exclude>
<OverWriteCheck></OverWriteCheck> <Undo></Undo>
<Results></Results> <Results></Results>
</SearchProvider> </SearchProvider>
</div> </div>

@ -0,0 +1,29 @@
import { useDialogDispatchers } from "@remix-ui/app"
import React from "react"
import { useContext } from "react"
import { SearchContext } from "../context/context"
import * as path from 'path'
export const Undo = () => {
const {
state,
undoReplace
} = useContext(SearchContext)
const { alert } = useDialogDispatchers()
const undo = async () => {
try{
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[`${state.workspace}/${state.currentFile}`] && state.undoBuffer[`${state.workspace}/${state.currentFile}`].visible ?
<button data-id={`undo-replace-${state.currentFile}`} disabled={!state.undoBuffer[`${state.workspace}/${state.currentFile}`].enabled} onClick={async() => await undo()} className="undo-button btn btn-secondary btn-block my-3">
<div className="fas fa-undo mr-2"></div>
Undo changes to {path.basename(state.undoBuffer[`${state.workspace}/${state.currentFile}`].path)}
</button> : null}
</>)
}

@ -1,3 +1,4 @@
import { useDialogDispatchers } from '@remix-ui/app'
import React, { useContext, useEffect, useRef, useState } from 'react' import React, { useContext, useEffect, useRef, useState } from 'react'
import { SearchContext } from '../../context/context' import { SearchContext } from '../../context/context'
import { SearchResult, SearchResultLine } from '../../types' import { SearchResult, SearchResultLine } from '../../types'
@ -9,7 +10,7 @@ interface ResultItemProps {
} }
export const ResultItem = (props: ResultItemProps) => { export const ResultItem = (props: ResultItemProps) => {
const { state, findText, disableForceReload, updateCount } = useContext( const { state, findText, disableForceReload, updateCount, replaceAllInFile } = useContext(
SearchContext SearchContext
) )
const [loading, setLoading] = useState<boolean>(false) const [loading, setLoading] = useState<boolean>(false)
@ -17,7 +18,7 @@ export const ResultItem = (props: ResultItemProps) => {
const [toggleExpander, setToggleExpander] = useState<boolean>(false) const [toggleExpander, setToggleExpander] = useState<boolean>(false)
const reloadTimeOut = useRef(null) const reloadTimeOut = useRef(null)
const subscribed = useRef(true) const subscribed = useRef(true)
const { modal } = useDialogDispatchers()
useEffect(() => { useEffect(() => {
reload() reload()
@ -41,10 +42,28 @@ export const ResultItem = (props: ResultItemProps) => {
useEffect(() => { useEffect(() => {
subscribed.current = true subscribed.current = true
return () => { return () => {
updateCount(0, props.file.filename)
subscribed.current = false 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 = () => { const reload = () => {
findText(props.file.filename).then(res => { findText(props.file.filename).then(res => {
if (subscribed.current) { if (subscribed.current) {
@ -84,14 +103,20 @@ export const ResultItem = (props: ResultItemProps) => {
</div> </div>
{loading ? <div className="loading">Loading...</div> : null} {loading ? <div className="loading">Loading...</div> : null}
{!toggleExpander && !loading ? ( {!toggleExpander && !loading ? (
<div className="p-1 search_plugin_wrap_summary"> <div className="search_plugin_wrap_summary">
{lines.map((line, index) => ( {state.replaceEnabled?
<div className="search_plugin_wrap_summary_replace">
<div data-id={`replace-all-${props.file.filename}`} onClick={async() => replace()} className='btn btn-secondary mb-2 btn-sm'>Replace all</div>
</div>
:null}
{lines.map((line, index) => (
index < state.maxLines ?
<ResultSummary <ResultSummary
setLoading={setLoading} setLoading={setLoading}
key={index} key={index}
searchResult={props.file} searchResult={props.file}
line={line} line={line}
/> />: null
))} ))}
</div> </div>
) : null} ) : null}

@ -47,15 +47,16 @@ export const ResultSummary = (props: ResultSummaryProps) => {
className='search_plugin_search_line pb-1' className='search_plugin_search_line pb-1'
> >
<div className='search_plugin_summary_left'>{lineItem.left.substring(lineItem.left.length - 20).trimStart()}</div> <div className='search_plugin_summary_left'>{lineItem.left.substring(lineItem.left.length - 20).trimStart()}</div>
<mark className={`search_plugin_summary_center ${state.replace? 'search_plugin_replace_strike':''}`}>{lineItem.center}</mark> <mark className={`search_plugin_summary_center ${state.replace && state.replaceEnabled? 'search_plugin_replace_strike':''}`}>{lineItem.center}</mark>
{state.replace? <mark className='search_plugin_replacement'>{state.replace}</mark>:<></>} {state.replace && state.replaceEnabled? <mark className='search_plugin_replacement'>{state.replace}</mark>:<></>}
<div className='search_plugin_summary_right'>{lineItem.right.substring(0, 100)}</div> <div className='search_plugin_summary_right'>{lineItem.right.substring(0, 100)}</div>
</div> </div>
{state.replaceEnabled?
<div className='search_plugin_search_control'> <div className='search_plugin_search_control'>
<div title="Replace" data-id={`replace-${props.searchResult.filename}-${lineItem.position.start.line}-${lineItem.position.start.column}`} onClick={async () => { <div title="Replace" data-id={`replace-${props.searchResult.filename}-${lineItem.position.start.line}-${lineItem.position.start.column}`} onClick={async () => {
replace(lineItem) replace(lineItem)
}} className="codicon codicon-find-replace" role="button" aria-label="Replace" aria-disabled="false"></div> }} className="codicon codicon-find-replace" role="button" aria-label="Replace" aria-disabled="false"></div>
</div> </div>:null}
</div> </div>
))} ))}
</> </>

@ -5,13 +5,13 @@ import { ResultItem } from './ResultItem'
export const Results = () => { export const Results = () => {
const { state } = useContext(SearchContext) const { state } = useContext(SearchContext)
return ( return (
<div data-id='search_results'> <div data-id='search_results' className='mt-2'>
{state.find ? <div className='search_plugin_result_count_number badge badge-pill badge-secondary'>{state.count} results</div>: null} {state.find ? <div className='search_plugin_result_count_number badge badge-pill badge-secondary'>showing {state.count} results {state.fileCount} in files</div>: null}
{state.count < state.maxResults && state.searchResults && {state.find && state.clipped? <div className='alert alert-warning mt-1'>The result set only shows a subset of all matches<br></br>Please narrow down your search.</div>: null}
{state.searchResults &&
state.searchResults.map((result, index) => { state.searchResults.map((result, index) => {
return <ResultItem key={index} file={result} /> return index <state.maxFiles? <ResultItem key={index} file={result} />: null
})} })}
{state.find && state.count >= state.maxResults? <div className='alert alert-warning mt-1'>Too many results to display.<br></br>Please narrow your search.</div>: null}
</div> </div>
) )
} }

@ -92,6 +92,9 @@ function getEOL(text) {
return u > w ? '\n' : '\r\n'; 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) => { export const replaceTextInLine = (str: string, searchResultLine: SearchResultLineLine, newText: string) => {
return str return str

@ -3,6 +3,7 @@ import { createContext, useReducer } from 'react'
import { import {
findLinesInStringWithMatch, findLinesInStringWithMatch,
getDirectory, getDirectory,
replaceAllInFile,
replaceTextInLine replaceTextInLine
} from '../components/results/SearchHelper' } from '../components/results/SearchHelper'
import { SearchReducer } from '../reducers/Reducer' import { SearchReducer } from '../reducers/Reducer'
@ -11,7 +12,8 @@ import {
SearchResult, SearchResult,
SearchResultLine, SearchResultLine,
SearchResultLineLine, SearchResultLineLine,
SearchingInitialState SearchingInitialState,
undoBufferRecord
} from '../types' } from '../types'
import { filePathFilter } from '@jsdevtools/file-path-filter' import { filePathFilter } from '@jsdevtools/file-path-filter'
import { escapeRegExp } from 'lodash' import { escapeRegExp } from 'lodash'
@ -20,6 +22,7 @@ export interface SearchingStateInterface {
state: SearchState state: SearchState
setFind: (value: string) => void setFind: (value: string) => void
setReplace: (value: string) => void setReplace: (value: string) => void
setReplaceEnabled: (value: boolean) => void
setInclude: (value: string) => void setInclude: (value: string) => void
setExclude: (value: string) => void setExclude: (value: string) => void
setCaseSensitive: (value: boolean) => void setCaseSensitive: (value: boolean) => void
@ -28,7 +31,7 @@ export interface SearchingStateInterface {
setSearchResults: (value: SearchResult[]) => void setSearchResults: (value: SearchResult[]) => void
findText: (path: string) => Promise<SearchResultLine[]> findText: (path: string) => Promise<SearchResultLine[]>
hightLightInPath: (result: SearchResult, line: SearchResultLineLine) => void hightLightInPath: (result: SearchResult, line: SearchResultLineLine) => void
replaceText: (result: SearchResult, line: SearchResultLineLine) => void replaceText: (result: SearchResult, line: SearchResultLineLine) => Promise<void>
reloadFile: (file: string) => void reloadFile: (file: string) => void
toggleCaseSensitive: () => void toggleCaseSensitive: () => void
toggleMatchWholeWord: () => void toggleMatchWholeWord: () => void
@ -36,6 +39,9 @@ export interface SearchingStateInterface {
setReplaceWithoutConfirmation: (value: boolean) => void setReplaceWithoutConfirmation: (value: boolean) => void
disableForceReload: (file: string) => void disableForceReload: (file: string) => void
updateCount: (count: number, file: string) => void updateCount: (count: number, file: string) => void
replaceAllInFile: (result: SearchResult) => Promise<void>
undoReplace: (buffer: undoBufferRecord) => Promise<void>
clearUndo: () => void
} }
export const SearchContext = createContext<SearchingStateInterface>(null) export const SearchContext = createContext<SearchingStateInterface>(null)
@ -63,6 +69,12 @@ export const SearchProvider = ({
payload: value payload: value
}) })
}, },
setReplaceEnabled: (value: boolean) => {
dispatch({
type: 'SET_REPLACE_ENABLED',
payload: value
})
},
setInclude: (value: string) => { setInclude: (value: string) => {
dispatch({ dispatch({
type: 'SET_INCLUDE', type: 'SET_INCLUDE',
@ -135,26 +147,32 @@ export const SearchProvider = ({
payload: file payload: file
}) })
}, },
setCurrentFile: (file: string) => {
dispatch({
type: 'SET_CURRENT_FILE',
payload: file
})
},
setCurrentWorkspace: (workspace: any) => {
dispatch({
type: 'SET_CURRENT_WORKSPACE',
payload: workspace
})
},
updateCount: (count: number, file: string) => { updateCount: (count: number, file: string) => {
dispatch({ dispatch({
type: 'UPDATE_COUNT', type: 'UPDATE_COUNT',
payload: {count, file} payload: { count, file }
}) })
}, },
findText: async (path: string) => { findText: async (path: string) => {
if (!plugin) return if (!plugin) return
try { try {
if (state.find.length < 3) return if (state.find.length < 1) return
const text = await plugin.call('fileManager', 'readFile', path) const text = await plugin.call('fileManager', 'readFile', path)
let flags = 'g' const result: SearchResultLine[] = findLinesInStringWithMatch(text, createRegExFromFind())
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)
return result return result
} catch (e) {} } catch (e) { }
}, },
hightLightInPath: async ( hightLightInPath: async (
result: SearchResult, result: SearchResult,
@ -162,6 +180,7 @@ export const SearchProvider = ({
) => { ) => {
await plugin.call('editor', 'discardHighlight') await plugin.call('editor', 'discardHighlight')
await plugin.call('editor', 'highlight', line.position, result.path) 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) => { replaceText: async (result: SearchResult, line: SearchResultLineLine) => {
try { try {
@ -172,30 +191,99 @@ export const SearchProvider = ({
'readFile', 'readFile',
result.path result.path
) )
const replaced = replaceTextInLine(content, line, state.replace)
await plugin.call( await plugin.call(
'fileManager', 'fileManager',
'setFile', 'setFile',
result.path, result.path,
replaceTextInLine(content, line, state.replace) replaced
) )
setUndoState(content, replaced, result.path)
} catch (e) { } catch (e) {
throw new Error(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)
},
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',
'readFile',
buffer.path
)
if (buffer.newContent !== content) {
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
)
},
clearUndo: () => {
dispatch ({
type: 'CLEAR_UNDO',
payload: undefined
})
} }
} }
const reloadStateForFile = async (file: string) => { const reloadStateForFile = async (file: string) => {
await value.reloadFile(file) await value.reloadFile(file)
} }
useEffect(() => { useEffect(() => {
plugin.on('filePanel', 'setWorkspace', () => { plugin.on('filePanel', 'setWorkspace', async (workspace) => {
value.setSearchResults(null) value.setSearchResults(null)
value.clearUndo()
value.setCurrentWorkspace(workspace.name)
}) })
plugin.on('fileManager', 'fileSaved', async file => { plugin.on('fileManager', 'fileSaved', async file => {
await reloadStateForFile(file) await reloadStateForFile(file)
await checkUndoState(file)
}) })
plugin.on('fileManager', 'currentFileChanged', async file => {
value.setCurrentFile(file)
await checkUndoState(file)
})
return () => { return () => {
plugin.off('fileManager', 'fileChanged') plugin.off('fileManager', 'fileChanged')
plugin.off('filePanel', 'setWorkspace') plugin.off('filePanel', 'setWorkspace')
@ -207,22 +295,61 @@ export const SearchProvider = ({
const results = [] const results = []
paths.split(',').forEach(path => { paths.split(',').forEach(path => {
path = path.trim() path = path.trim()
if(path.startsWith('*.')) path = path.replace(/(\*\.)/g, '**/*.') if (path.startsWith('*.')) path = path.replace(/(\*\.)/g, '**/*.')
if(path.endsWith('/*') && !path.endsWith('/**/*')) path = path.replace(/(\*)/g, '**/*.*') if (path.endsWith('/*') && !path.endsWith('/**/*')) path = path.replace(/(\*)/g, '**/*.*')
results.push(path) results.push(path)
}) })
return results 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.name
}
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(() => { useEffect(() => {
if (state.find) { if (state.find) {
(async () => { (async () => {
const files = await getDirectory('/', plugin) const files = await getDirectory('/', plugin)
const pathFilter: any = {} const pathFilter: any = {}
if (state.include){ if (state.include) {
pathFilter.include = setGlobalExpression(state.include) pathFilter.include = setGlobalExpression(state.include)
} }
if (state.exclude){ if (state.exclude) {
pathFilter.exclude = setGlobalExpression(state.exclude) pathFilter.exclude = setGlobalExpression(state.exclude)
} }
const filteredFiles = files.filter(filePathFilter(pathFilter)).map(file => { const filteredFiles = files.filter(filePathFilter(pathFilter)).map(file => {

@ -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) => { export const SearchReducer = (state: SearchState = SearchingInitialState, action: Action) => {
switch (action.type) { switch (action.type) {
@ -15,6 +15,12 @@ export const SearchReducer = (state: SearchState = SearchingInitialState, action
replace: action.payload, replace: action.payload,
} }
case 'SET_REPLACE_ENABLED':
return {
...state,
replaceEnabled: action.payload,
}
case 'SET_INCLUDE': case 'SET_INCLUDE':
return { return {
...state, ...state,
@ -35,21 +41,60 @@ export const SearchReducer = (state: SearchState = SearchingInitialState, action
searchResults: action.payload, searchResults: action.payload,
count: 0 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(),
enabled: true,
visible: true
}
state.undoBuffer[`${undoState.workspace}/${undoState.path}`] = undoState
return {
...state,
}
}
case 'CLEAR_UNDO': {
state.undoBuffer = []
return {
...state,
}
}
case 'UPDATE_COUNT': case 'UPDATE_COUNT':
if (state.searchResults) { if (state.searchResults) {
const findFile = state.searchResults.find(file => file.filename === action.payload.file) const findFile = state.searchResults.find(file => file.filename === action.payload.file)
let count = 0 let count = 0
let fileCount = 0
let clipped = false
if (findFile) { if (findFile) {
findFile.count = action.payload.count findFile.count = action.payload.count
} }
state.searchResults.forEach(file => { state.searchResults.forEach(file => {
if (file.count) { if (file.count) {
count += file.count if(file.count > state.maxLines) {
clipped = true
count += state.maxLines
}else{
count += file.count
}
fileCount++
} }
}) })
return { return {
...state, ...state,
count: count count: count,
fileCount,
clipped
} }
} else { } else {
return state return state
@ -85,6 +130,16 @@ export const SearchReducer = (state: SearchState = SearchingInitialState, action
return { return {
...state, ...state,
} }
case 'SET_CURRENT_FILE':
return {
...state,
currentFile: action.payload,
}
case 'SET_CURRENT_WORKSPACE':
return {
...state,
workspace: action.payload,
}
case 'RELOAD_FILE': case 'RELOAD_FILE':
if (state.searchResults) { if (state.searchResults) {
const findFile = state.searchResults.find(file => file.filename === action.payload) const findFile = state.searchResults.find(file => file.filename === action.payload)

@ -21,7 +21,7 @@
.search_plugin_find-part { .search_plugin_find-part {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding: 2px; padding-top: 5px;
} }
.search_plugin_controls { .search_plugin_controls {
@ -64,6 +64,7 @@
.search_plugin_search_tab mark { .search_plugin_search_tab mark {
padding: 0; padding: 0;
white-space: pre;
} }
.search_plugin_search_tab .search_plugin_search_line_container .search_plugin_search_control { .search_plugin_search_tab .search_plugin_search_line_container .search_plugin_search_control {
@ -103,4 +104,33 @@
.search_plugin_search_tab .search_plugin_result_count_number { .search_plugin_search_tab .search_plugin_result_count_number {
font-size: x-small; 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;
}
.search_plugin_wrap_summary_replace {
display: flex;
flex-direction: row;
justify-content: flex-end;
}
.undo-button {
white-space: pre;
text-overflow: ellipsis;
overflow: hidden;
} }

@ -35,10 +35,20 @@ export interface SearchResult {
count: number count: number
} }
export interface undoBufferRecord {
workspace: string,
path: string,
newContent: string,
timeStamp: number,
oldContent: string,
enabled: boolean,
visible: boolean
}
export interface SearchState { export interface SearchState {
find: string, find: string,
searchResults: SearchResult[], searchResults: SearchResult[],
replace: string, replace: string,
replaceEnabled: boolean,
include: string, include: string,
exclude: string, exclude: string,
casesensitive: boolean, casesensitive: boolean,
@ -47,7 +57,13 @@ export interface SearchState {
useRegExp: boolean, useRegExp: boolean,
timeStamp: number, timeStamp: number,
count: number, count: number,
maxResults: number fileCount: number,
maxFiles: number,
maxLines: number
clipped: boolean,
undoBuffer: Record<string, undoBufferRecord>[],
currentFile: string,
workspace: string
} }
export const SearchingInitialState: SearchState = { export const SearchingInitialState: SearchState = {
@ -55,6 +71,7 @@ export const SearchingInitialState: SearchState = {
replace: '', replace: '',
include: '', include: '',
exclude: '', exclude: '',
replaceEnabled: false,
searchResults: [], searchResults: [],
casesensitive: false, casesensitive: false,
matchWord: false, matchWord: false,
@ -62,5 +79,11 @@ export const SearchingInitialState: SearchState = {
replaceWithOutConfirmation: false, replaceWithOutConfirmation: false,
timeStamp: 0, timeStamp: 0,
count: 0, count: 0,
maxResults: 500 fileCount: 0,
maxFiles: 5000,
maxLines: 5000,
clipped: false,
undoBuffer: null,
currentFile: '',
workspace: ''
} }
Loading…
Cancel
Save