Merge pull request #2170 from ethereum/searchfixes

search fixes
pull/2191/head^2
Rob 3 years ago committed by GitHub
commit 6227253002
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'
module.exports = {
'@disabled': true,
before: function (browser: NightwatchBrowser, done: VoidFunction) {
init(browser, done, 'http://127.0.0.1:8080', true)
},
@ -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)
@ -90,6 +92,119 @@ module.exports = {
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) {
browser
.clearValue('*[id="search_input"]')

@ -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',

@ -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

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

@ -4,10 +4,10 @@ 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',
icon: 'assets/img/search_icon.webp',
description: '',
kind: '',
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
'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)

@ -1,3 +1,5 @@
import { IRange } from "monaco-editor";
export interface Action {
type: string;
payload: Record<string, any>
@ -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',

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

@ -18,7 +18,6 @@ export const Find = () => {
return (
<>
<div className="search_plugin_find-part">
<label>search</label>
<div className="search_plugin_search-input">
<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 (
<>
<div className="search_plugin_find-part">
<label>include</label>
<div className="search_plugin_find-part pl-3">
<label className='mt-2'>Files to include</label>
<input
id='search_include'
placeholder="Include ie contracts/**/*.sol"

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

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

@ -7,6 +7,8 @@ import { Include } from './Include'
import { Exclude } from './Exclude'
import { Replace } from './Replace'
import { OverWriteCheck } from './OverWriteCheck'
import { FindContainer } from './FindContainer'
import { Undo } from './Undo'
export const SearchTab = props => {
@ -14,13 +16,12 @@ const plugin = props.plugin
return (
<>
<div className="search_plugin_search_tab pl-2 pr-2">
<div className="search_plugin_search_tab px-2">
<SearchProvider plugin={plugin}>
<Find></Find>
<Replace></Replace>
<FindContainer></FindContainer>
<Include></Include>
<Exclude></Exclude>
<OverWriteCheck></OverWriteCheck>
<Undo></Undo>
<Results></Results>
</SearchProvider>
</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 { 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<boolean>(false)
@ -17,7 +18,7 @@ export const ResultItem = (props: ResultItemProps) => {
const [toggleExpander, setToggleExpander] = useState<boolean>(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) {
@ -84,14 +103,20 @@ export const ResultItem = (props: ResultItemProps) => {
</div>
{loading ? <div className="loading">Loading...</div> : null}
{!toggleExpander && !loading ? (
<div className="p-1 search_plugin_wrap_summary">
{lines.map((line, index) => (
<div className="search_plugin_wrap_summary">
{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
setLoading={setLoading}
key={index}
searchResult={props.file}
line={line}
/>
/>: null
))}
</div>
) : null}

@ -47,15 +47,16 @@ export const ResultSummary = (props: ResultSummaryProps) => {
className='search_plugin_search_line pb-1'
>
<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>
{state.replace? <mark className='search_plugin_replacement'>{state.replace}</mark>:<></>}
<mark className={`search_plugin_summary_center ${state.replace && state.replaceEnabled? 'search_plugin_replace_strike':''}`}>{lineItem.center}</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>
{state.replaceEnabled?
<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 () => {
replace(lineItem)
}} className="codicon codicon-find-replace" role="button" aria-label="Replace" aria-disabled="false"></div>
</div>
</div>:null}
</div>
))}
</>

@ -5,13 +5,13 @@ import { ResultItem } from './ResultItem'
export const Results = () => {
const { state } = useContext(SearchContext)
return (
<div data-id='search_results'>
{state.find ? <div className='search_plugin_result_count_number badge badge-pill badge-secondary'>{state.count} results</div>: null}
{state.count < state.maxResults && state.searchResults &&
<div data-id='search_results' className='mt-2'>
{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.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) => {
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>
)
}

@ -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

@ -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'
@ -20,6 +22,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
@ -28,7 +31,7 @@ export interface SearchingStateInterface {
setSearchResults: (value: SearchResult[]) => void
findText: (path: string) => Promise<SearchResultLine[]>
hightLightInPath: (result: SearchResult, line: SearchResultLineLine) => void
replaceText: (result: SearchResult, line: SearchResultLineLine) => void
replaceText: (result: SearchResult, line: SearchResultLineLine) => Promise<void>
reloadFile: (file: string) => void
toggleCaseSensitive: () => void
toggleMatchWholeWord: () => void
@ -36,6 +39,9 @@ export interface SearchingStateInterface {
setReplaceWithoutConfirmation: (value: boolean) => void
disableForceReload: (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)
@ -63,6 +69,12 @@ export const SearchProvider = ({
payload: value
})
},
setReplaceEnabled: (value: boolean) => {
dispatch({
type: 'SET_REPLACE_ENABLED',
payload: value
})
},
setInclude: (value: string) => {
dispatch({
type: 'SET_INCLUDE',
@ -135,26 +147,32 @@ export const SearchProvider = ({
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) => {
dispatch({
type: 'UPDATE_COUNT',
payload: {count, file}
payload: { count, file }
})
},
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
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,
@ -162,6 +180,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 {
@ -172,30 +191,99 @@ 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)
},
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) => {
await value.reloadFile(file)
await value.reloadFile(file)
}
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')
@ -207,22 +295,61 @@ 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 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(() => {
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 => {

@ -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) {
@ -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,
@ -35,21 +41,60 @@ 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(),
enabled: true,
visible: true
}
state.undoBuffer[`${undoState.workspace}/${undoState.path}`] = 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
@ -85,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)

@ -21,7 +21,7 @@
.search_plugin_find-part {
display: flex;
flex-direction: column;
padding: 2px;
padding-top: 5px;
}
.search_plugin_controls {
@ -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 {
@ -103,4 +104,33 @@
.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;
}
.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
}
export interface undoBufferRecord {
workspace: string,
path: string,
newContent: string,
timeStamp: number,
oldContent: string,
enabled: boolean,
visible: boolean
}
export interface SearchState {
find: string,
searchResults: SearchResult[],
replace: string,
replaceEnabled: boolean,
include: string,
exclude: string,
casesensitive: boolean,
@ -47,7 +57,13 @@ export interface SearchState {
useRegExp: boolean,
timeStamp: 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 = {
@ -55,6 +71,7 @@ export const SearchingInitialState: SearchState = {
replace: '',
include: '',
exclude: '',
replaceEnabled: false,
searchResults: [],
casesensitive: false,
matchWord: false,
@ -62,5 +79,11 @@ export const SearchingInitialState: SearchState = {
replaceWithOutConfirmation: false,
timeStamp: 0,
count: 0,
maxResults: 500
fileCount: 0,
maxFiles: 5000,
maxLines: 5000,
clipped: false,
undoBuffer: null,
currentFile: '',
workspace: ''
}
Loading…
Cancel
Save