diff --git a/apps/remix-ide-e2e/src/tests/search.test.ts b/apps/remix-ide-e2e/src/tests/search.test.ts
new file mode 100644
index 0000000000..19fe83da3c
--- /dev/null
+++ b/apps/remix-ide-e2e/src/tests/search.test.ts
@@ -0,0 +1,122 @@
+'use strict'
+
+import { NightwatchBrowser } from 'nightwatch'
+import init from '../helpers/init'
+
+module.exports = {
+
+ before: function (browser: NightwatchBrowser, done: VoidFunction) {
+ init(browser, done, 'http://127.0.0.1:8080', true)
+ },
+ '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)
+ .waitForElementContainsText('*[data-id="search_results"]', '3_BALLOT.SOL', 60000)
+ .waitForElementContainsText('*[data-id="search_results"]', 'contracts', 60000)
+ .waitForElementContainsText('*[data-id="search_results"]', 'README.TXT', 60000)
+ .waitForElementContainsText('*[data-id="search_results"]', 'file must')
+ .waitForElementContainsText('*[data-id="search_results"]', 'be compiled')
+ .waitForElementContainsText('*[data-id="search_results"]', 'that person al')
+ .waitForElementContainsText('*[data-id="search_results"]', 'sender.voted')
+ .waitForElementContainsText('*[data-id="search_results"]', 'read')
+ .elements('css selector','.search_plugin_search_line', (res) => {
+ Array.isArray(res.value) && browser.assert.equal(res.value.length, 6)
+ })
+ },
+ 'Should find regex': function (browser: NightwatchBrowser) {
+ browser
+ .waitForElementVisible('*[data-id="search_use_regex"]').click('*[data-id="search_use_regex"]')
+ .waitForElementVisible('*[id="search_input"]')
+ .clearValue('*[id="search_input"]')
+ .setValue('*[id="search_input"]', '^contract').pause(1000)
+ .waitForElementContainsText('*[data-id="search_results"]', '3_BALLOT.SOL', 60000)
+ .waitForElementContainsText('*[data-id="search_results"]', '2_OWNER.SOL', 60000)
+ .waitForElementContainsText('*[data-id="search_results"]', '1_STORAGE.SOL', 60000)
+ .waitForElementContainsText('*[data-id="search_results"]', '4_BALLOT_TEST.SOL', 60000)
+ .waitForElementContainsText('*[data-id="search_results"]', 'tests', 60000)
+ .elements('css selector','.search_plugin_search_line', (res) => {
+ Array.isArray(res.value) && browser.assert.equal(res.value.length, 4)
+ })
+ },
+ '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"]')
+ .elements('css selector','.search_plugin_search_line', (res) => {
+ Array.isArray(res.value) && browser.assert.equal(res.value.length, 0)
+ })
+ .clearValue('*[id="search_input"]')
+ .setValue('*[id="search_input"]', 'Contract').pause(1000)
+ .elements('css selector','.search_plugin_search_line', (res) => {
+ Array.isArray(res.value) && browser.assert.equal(res.value.length, 6)
+ })
+ .waitForElementContainsText('*[data-id="search_results"]', 'DEPLOY_ETHERS.JS', 60000)
+ .waitForElementContainsText('*[data-id="search_results"]', 'DEPLOY_WEB3.JS', 60000)
+ .waitForElementContainsText('*[data-id="search_results"]', 'scripts', 60000)
+ },
+ '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"]')
+ .clearValue('*[id="search_input"]')
+ .setValue('*[id="search_input"]', 'contract').pause(1000)
+ .elements('css selector','.search_plugin_search_line', (res) => {
+ Array.isArray(res.value) && browser.assert.equal(res.value.length, 27)
+ })
+ },
+ 'Should replace text': function (browser: NightwatchBrowser) {
+ browser
+ .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)
+ .waitForElementVisible('*[data-id="replace-contracts/2_Owner.sol-30-71"]')
+ .click('*[data-id="replace-contracts/2_Owner.sol-30-71"]').pause(2000).
+ modalFooterOKClick('confirmreplace').pause(2000).
+ getEditorValue((content) => {
+ browser.assert.ok(content.includes('replacing deployer for a constructor'), 'should replace text ok')
+ })
+ },
+ '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)
+ .setValue('*[id="search_replace"]', '2').pause(1000)
+ .waitForElementVisible('*[data-id="contracts/2_Owner.sol-30-71"]')
+ .moveToElement('*[data-id="contracts/2_Owner.sol-30-71"]', 10, 10)
+ .waitForElementVisible('*[data-id="replace-contracts/2_Owner.sol-30-71"]')
+ .click('*[data-id="replace-contracts/2_Owner.sol-30-71"]').pause(2000).
+ getEditorValue((content) => {
+ browser.assert.ok(content.includes('replacing2 deployer for a constructor'), 'should replace text ok')
+ })
+ },
+ 'Should find text with include': function (browser: NightwatchBrowser) {
+ browser
+ .clearValue('*[id="search_input"]')
+ .setValue('*[id="search_input"]', 'contract').pause(1000)
+ .setValue('*[id="search_include"]', 'contracts/**').pause(2000)
+ .elements('css selector','.search_plugin_search_line', (res) => {
+ Array.isArray(res.value) && browser.assert.equal(res.value.length, 4)
+ })
+ },
+ 'Should find text with exclude': function (browser: NightwatchBrowser) {
+ browser
+ .clearValue('*[id="search_include"]').pause(2000)
+ .setValue('*[id="search_include"]', '**').pause(2000)
+ .elements('css selector','.search_plugin_search_line', (res) => {
+ Array.isArray(res.value) && browser.assert.equal(res.value.length, 26)
+ })
+ .setValue('*[id="search_exclude"]', ',contracts/**').pause(2000)
+ .elements('css selector','.search_plugin_search_line', (res) => {
+ Array.isArray(res.value) && browser.assert.equal(res.value.length, 22)
+ })
+ },
+ 'should clear search': function (browser: NightwatchBrowser) {
+ browser
+ .waitForElementVisible('*[id="search_input"]')
+ .setValue('*[id="search_input"]', 'nodata').pause(1000)
+ .elements('css selector','.search_plugin_search_line', (res) => {
+ Array.isArray(res.value) && browser.assert.equal(res.value.length, 0)
+ })
+ }
+}
\ No newline at end of file
diff --git a/apps/remix-ide/src/app.js b/apps/remix-ide/src/app.js
index 0e936467d6..732ef24c4a 100644
--- a/apps/remix-ide/src/app.js
+++ b/apps/remix-ide/src/app.js
@@ -30,6 +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')
@@ -147,6 +148,9 @@ class AppComponent {
// ----------------- Storage plugin ---------------------------------
const storagePlugin = new StoragePlugin()
+ //----- search
+ const search = new SearchPlugin()
+
// ----------------- import content service ------------------------
const contentImport = new CompilerImports()
@@ -221,7 +225,8 @@ class AppComponent {
dGitProvider,
storagePlugin,
hardhatProvider,
- this.walkthroughService
+ this.walkthroughService,
+ search
])
// LAYOUT & SYSTEM VIEWS
@@ -332,8 +337,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'])
- // await this.appManager.activatePlugin(['scriptRunner'])
+ await this.appManager.activatePlugin(['walkthrough','storage', 'search'])
this.appManager.on(
'filePanel',
diff --git a/apps/remix-ide/src/app/components/vertical-icons.tsx b/apps/remix-ide/src/app/components/vertical-icons.tsx
index bc70fd812c..d8fe5706f8 100644
--- a/apps/remix-ide/src/app/components/vertical-icons.tsx
+++ b/apps/remix-ide/src/app/components/vertical-icons.tsx
@@ -29,7 +29,7 @@ export class VerticalIcons extends Plugin {
}
renderComponent () {
- const fixedOrder = ['filePanel', 'solidity','udapp', 'debugger', 'solidityStaticAnalysis', 'solidityUnitTesting', 'pluginManager']
+ const fixedOrder = ['filePanel', 'search', 'solidity','udapp', 'debugger', 'solidityStaticAnalysis', 'solidityUnitTesting', 'pluginManager']
const divived = Object.values(this.icons).map((value) => { return {
...value,
diff --git a/apps/remix-ide/src/app/tabs/search.tsx b/apps/remix-ide/src/app/tabs/search.tsx
new file mode 100644
index 0000000000..6914ed0150
--- /dev/null
+++ b/apps/remix-ide/src/app/tabs/search.tsx
@@ -0,0 +1,32 @@
+import { ViewPlugin } from '@remixproject/engine-web'
+import * as packageJson from '../../../../../package.json'
+import React from 'react' // eslint-disable-line
+import { SearchTab } from '@remix-ui/search'
+const profile = {
+ name: 'search',
+ displayName: 'Search',
+ methods: [''],
+ events: [],
+ icon: 'assets/img/Search_Icon.svg',
+ description: '',
+ kind: '',
+ location: 'sidePanel',
+ documentation: '',
+ version: packageJson.version
+ }
+
+export class SearchPlugin extends ViewPlugin {
+
+ constructor () {
+ super(profile)
+ }
+
+ render() {
+ return (
+
+
+
+ );
+ }
+
+}
\ No newline at end of file
diff --git a/apps/remix-ide/src/assets/img/Search_Icon.svg b/apps/remix-ide/src/assets/img/Search_Icon.svg
new file mode 100644
index 0000000000..00a6fcde04
--- /dev/null
+++ b/apps/remix-ide/src/assets/img/Search_Icon.svg
@@ -0,0 +1,77 @@
+
+
+
+
diff --git a/apps/remix-ide/src/remixAppManager.js b/apps/remix-ide/src/remixAppManager.js
index 5f0fac101f..4ee5d60905 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']
+ 'filePanel', 'terminal', 'settings', 'pluginManager', 'tabs', 'udapp', 'dGitProvider', 'solidity-logic', 'gistHandler', 'layout', 'notification', 'permissionhandler', 'walkthrough', 'storage', 'search']
const dependentModules = ['git', 'hardhat', 'slither'] // module which shouldn't be manually activated (e.g git is activated by remixd)
diff --git a/libs/remix-ui/app/src/index.ts b/libs/remix-ui/app/src/index.ts
index 47c95554fd..45082b0fec 100644
--- a/libs/remix-ui/app/src/index.ts
+++ b/libs/remix-ui/app/src/index.ts
@@ -1,6 +1,6 @@
export { default as RemixApp } from './lib/remix-app/remix-app'
export { dispatchModalContext, AppContext } from './lib/remix-app/context/context'
-export { ModalProvider } from './lib/remix-app/context/provider'
+export { ModalProvider, useDialogDispatchers } from './lib/remix-app/context/provider'
export { AppModal } from './lib/remix-app/interface/index'
export { AlertModal } from './lib/remix-app/interface/index'
export { ModalTypes } from './lib/remix-app/types/index'
diff --git a/libs/remix-ui/app/src/lib/remix-app/components/modals/modal-wrapper.tsx b/libs/remix-ui/app/src/lib/remix-app/components/modals/modal-wrapper.tsx
index cf5456dc38..12644f5fa7 100644
--- a/libs/remix-ui/app/src/lib/remix-app/components/modals/modal-wrapper.tsx
+++ b/libs/remix-ui/app/src/lib/remix-app/components/modals/modal-wrapper.tsx
@@ -10,6 +10,7 @@ interface ModalWrapperProps extends ModalDialogProps {
const ModalWrapper = (props: ModalWrapperProps) => {
const [state, setState] = useState()
const ref = useRef()
+ const data = useRef()
const onFinishPrompt = async () => {
if (ref.current === undefined) {
@@ -21,7 +22,7 @@ const ModalWrapper = (props: ModalWrapperProps) => {
}
const onOkFn = async () => {
- (props.okFn) ? props.okFn() : props.resolve(true)
+ (props.okFn) ? props.okFn(data.current) : props.resolve(data.current || true)
}
const onCancelFn = async () => {
@@ -37,6 +38,7 @@ const ModalWrapper = (props: ModalWrapperProps) => {
}
useEffect(() => {
+ data.current = props.data
if (props.modalType) {
switch (props.modalType) {
case ModalTypes.prompt:
diff --git a/libs/remix-ui/app/src/lib/remix-app/context/provider.tsx b/libs/remix-ui/app/src/lib/remix-app/context/provider.tsx
index 5d9a3a5815..2aca27c1be 100644
--- a/libs/remix-ui/app/src/lib/remix-app/context/provider.tsx
+++ b/libs/remix-ui/app/src/lib/remix-app/context/provider.tsx
@@ -15,18 +15,18 @@ export const ModalProvider = ({ children = [], reducer = modalReducer, initialSt
})
}
- const modal = (data: AppModal) => {
- const { id, title, message, okLabel, okFn, cancelLabel, cancelFn, modalType, defaultValue, hideFn } = data
+ const modal = (modalData: AppModal) => {
+ const { id, title, message, okLabel, okFn, cancelLabel, cancelFn, modalType, defaultValue, hideFn, data } = modalData
return new Promise((resolve, reject) => {
dispatch({
type: modalActionTypes.setModal,
- payload: { id, title, message, okLabel, okFn, cancelLabel, cancelFn, modalType: modalType || ModalTypes.default, defaultValue: defaultValue, hideFn, resolve, next: onNextFn }
+ payload: { id, title, message, okLabel, okFn, cancelLabel, cancelFn, modalType: modalType || ModalTypes.default, defaultValue: defaultValue, hideFn, resolve, next: onNextFn, data }
})
})
}
- const alert = (data: AlertModal) => {
- return modal({ id: data.id, title: data.title || 'Alert', message: data.message || data.title, okLabel: 'OK', okFn: (value?:any) => {}, cancelLabel: '', cancelFn: () => {} })
+ const alert = (modalData: AlertModal) => {
+ return modal({ id: modalData.id, title: modalData.title || 'Alert', message: modalData.message || modalData.title, okLabel: 'OK', okFn: (value?:any) => {}, cancelLabel: '', cancelFn: () => {} })
}
const handleHideModal = () => {
diff --git a/libs/remix-ui/app/src/lib/remix-app/interface/index.ts b/libs/remix-ui/app/src/lib/remix-app/interface/index.ts
index af25ce13b9..f31536678e 100644
--- a/libs/remix-ui/app/src/lib/remix-app/interface/index.ts
+++ b/libs/remix-ui/app/src/lib/remix-app/interface/index.ts
@@ -15,7 +15,8 @@ export interface AppModal {
defaultValue?: string
hideFn?: () => void,
resolve?: (value?:any) => void,
- next?: () => void
+ next?: () => void,
+ data?: any
}
export interface AlertModal {
diff --git a/libs/remix-ui/app/src/lib/remix-app/reducer/modals.ts b/libs/remix-ui/app/src/lib/remix-app/reducer/modals.ts
index 8a1c89ac79..a50a3dbd66 100644
--- a/libs/remix-ui/app/src/lib/remix-app/reducer/modals.ts
+++ b/libs/remix-ui/app/src/lib/remix-app/reducer/modals.ts
@@ -20,7 +20,8 @@ export const modalReducer = (state: ModalState = ModalInitialState, action: Moda
defaultValue: action.payload.defaultValue,
hideFn: action.payload.hideFn,
resolve: action.payload.resolve,
- next: action.payload.next
+ next: action.payload.next,
+ data: action.payload.data
}
const modalList: AppModal[] = state.modals.slice()
diff --git a/libs/remix-ui/modal-dialog/src/lib/types/index.ts b/libs/remix-ui/modal-dialog/src/lib/types/index.ts
index db473683d1..a8793971d7 100644
--- a/libs/remix-ui/modal-dialog/src/lib/types/index.ts
+++ b/libs/remix-ui/modal-dialog/src/lib/types/index.ts
@@ -14,5 +14,6 @@ export interface ModalDialogProps {
handleHide: (hideState?: boolean) => void,
children?: React.ReactNode,
resolve?: (value?:any) => void,
- next?: () => void
+ next?: () => void,
+ data?: any
}
diff --git a/libs/remix-ui/search/.babelrc b/libs/remix-ui/search/.babelrc
new file mode 100644
index 0000000000..64a3748691
--- /dev/null
+++ b/libs/remix-ui/search/.babelrc
@@ -0,0 +1,4 @@
+{
+ "presets": ["@nrwl/react/babel"],
+ "plugins": []
+}
\ No newline at end of file
diff --git a/libs/remix-ui/search/.eslintrc.json b/libs/remix-ui/search/.eslintrc.json
new file mode 100644
index 0000000000..1587c6172a
--- /dev/null
+++ b/libs/remix-ui/search/.eslintrc.json
@@ -0,0 +1,19 @@
+{
+ "env": {
+ "browser": true,
+ "es6": true
+ },
+ "ignorePatterns": ["!**/*"],
+ "extends": "../../../.eslintrc.json",
+ "globals": {
+ "Atomics": "readonly",
+ "SharedArrayBuffer": "readonly"
+ },
+ "parserOptions": {
+ "ecmaVersion": 11,
+ "sourceType": "module"
+ },
+ "rules": {
+ "standard/no-callback-literal": "off"
+ }
+}
\ No newline at end of file
diff --git a/libs/remix-ui/search/.prettierrc b/libs/remix-ui/search/.prettierrc
new file mode 100644
index 0000000000..591b168cb7
--- /dev/null
+++ b/libs/remix-ui/search/.prettierrc
@@ -0,0 +1,5 @@
+{
+ "tabWidth": 2,
+ "singleQuote": true,
+ "semi": false
+}
\ No newline at end of file
diff --git a/libs/remix-ui/search/src/index.ts b/libs/remix-ui/search/src/index.ts
new file mode 100644
index 0000000000..8617e2ddff
--- /dev/null
+++ b/libs/remix-ui/search/src/index.ts
@@ -0,0 +1 @@
+export { SearchTab } from './lib/components/Search';
\ No newline at end of file
diff --git a/libs/remix-ui/search/src/lib/components/Exclude.tsx b/libs/remix-ui/search/src/lib/components/Exclude.tsx
new file mode 100644
index 0000000000..36d3bdd103
--- /dev/null
+++ b/libs/remix-ui/search/src/lib/components/Exclude.tsx
@@ -0,0 +1,32 @@
+import React, { useContext, useEffect, useRef, useState } from 'react'
+import { SearchContext } from '../context/context'
+
+export const Exclude = props => {
+ const { setExclude, state } = useContext(SearchContext)
+ const [excludeInput, setExcludeInput] = useState('.git/**/*,.deps/**/*')
+ const timeOutId = useRef(null)
+ const change = e => {
+ setExcludeInput(e.target.value)
+ clearTimeout(timeOutId.current)
+ timeOutId.current = setTimeout(() => setExclude(e.target.value), 500)
+ }
+
+ useEffect(() => {
+ setExclude(excludeInput)
+ }, [])
+
+ return (
+ <>
+
+
+
+
+ >
+ )
+}
diff --git a/libs/remix-ui/search/src/lib/components/Find.tsx b/libs/remix-ui/search/src/lib/components/Find.tsx
new file mode 100644
index 0000000000..42343fce6e
--- /dev/null
+++ b/libs/remix-ui/search/src/lib/components/Find.tsx
@@ -0,0 +1,77 @@
+import React, { useContext, useRef } from 'react'
+import { SearchContext } from '../context/context'
+
+export const Find = () => {
+ const {
+ setFind,
+ state,
+ toggleCaseSensitive,
+ toggleMatchWholeWord,
+ toggleUseRegex
+ } = useContext(SearchContext)
+ const timeOutId = useRef(null)
+ const change = e => {
+ clearTimeout(timeOutId.current)
+ timeOutId.current = setTimeout(() => setFind(e.target.value), 500)
+ }
+
+ return (
+ <>
+
+
+
+
+
+
{
+ toggleCaseSensitive()
+ }}
+ >
+
{
+ toggleMatchWholeWord()
+ }}
+ >
+
{
+ toggleUseRegex()
+ }}
+ >
+
+
+
+ >
+ )
+}
diff --git a/libs/remix-ui/search/src/lib/components/Include.tsx b/libs/remix-ui/search/src/lib/components/Include.tsx
new file mode 100644
index 0000000000..ab50233885
--- /dev/null
+++ b/libs/remix-ui/search/src/lib/components/Include.tsx
@@ -0,0 +1,28 @@
+import React, { useContext, useRef, useState } from 'react'
+import { SearchContext } from '../context/context'
+
+export const Include = props => {
+ const { setInclude } = useContext(SearchContext)
+ const [includeInput, setIncludeInput] = useState('')
+ const timeOutId = useRef(null)
+ const change = e => {
+ setIncludeInput(e.target.value)
+ clearTimeout(timeOutId.current)
+ timeOutId.current = setTimeout(() => setInclude(e.target.value), 500)
+ }
+
+ return (
+ <>
+
+
+
+
+ >
+ )
+}
diff --git a/libs/remix-ui/search/src/lib/components/OverWriteCheck.tsx b/libs/remix-ui/search/src/lib/components/OverWriteCheck.tsx
new file mode 100644
index 0000000000..ece3f26dc2
--- /dev/null
+++ b/libs/remix-ui/search/src/lib/components/OverWriteCheck.tsx
@@ -0,0 +1,32 @@
+import React, { useContext } from 'react'
+import { SearchContext } from '../context/context'
+
+export const OverWriteCheck = props => {
+ const { setReplaceWithoutConfirmation } = useContext(SearchContext)
+
+ const change = e => {
+ setReplaceWithoutConfirmation(e.target.checked)
+ }
+
+ return (
+ <>
+
+
+
+
+
+
+ >
+ )
+}
diff --git a/libs/remix-ui/search/src/lib/components/Replace.tsx b/libs/remix-ui/search/src/lib/components/Replace.tsx
new file mode 100644
index 0000000000..7c39191e6d
--- /dev/null
+++ b/libs/remix-ui/search/src/lib/components/Replace.tsx
@@ -0,0 +1,25 @@
+import React, { useContext, useRef } from 'react'
+import { SearchContext } from '../context/context'
+
+export const Replace = props => {
+ const { setReplace } = useContext(SearchContext)
+ const timeOutId = useRef(null)
+ const change = e => {
+ clearTimeout(timeOutId.current)
+ timeOutId.current = setTimeout(() => setReplace(e.target.value), 500)
+ }
+
+ return (
+ <>
+
+
+
+
+ >
+ )
+}
diff --git a/libs/remix-ui/search/src/lib/components/Search.tsx b/libs/remix-ui/search/src/lib/components/Search.tsx
new file mode 100644
index 0000000000..8349aef754
--- /dev/null
+++ b/libs/remix-ui/search/src/lib/components/Search.tsx
@@ -0,0 +1,29 @@
+import React from 'react'
+import { SearchProvider } from '../context/context'
+import { Find } from './Find'
+import { Results } from './results/Results'
+import '../search.css'
+import { Include } from './Include'
+import { Exclude } from './Exclude'
+import { Replace } from './Replace'
+import { OverWriteCheck } from './OverWriteCheck'
+
+export const SearchTab = props => {
+
+const plugin = props.plugin
+
+return (
+ <>
+
+
+
+
+
+
+
+
+
+
+ >
+ )
+}
diff --git a/libs/remix-ui/search/src/lib/components/results/ResultFileName.tsx b/libs/remix-ui/search/src/lib/components/results/ResultFileName.tsx
new file mode 100644
index 0000000000..303e8da21e
--- /dev/null
+++ b/libs/remix-ui/search/src/lib/components/results/ResultFileName.tsx
@@ -0,0 +1,27 @@
+import React, { useEffect, useState } from 'react'
+import { SearchResult } from '../../types'
+import { getPathIcon } from '@remix-ui/helper'
+import * as path from 'path'
+interface ResultItemProps {
+ file: SearchResult
+}
+
+export const ResultFileName = (props: ResultItemProps) => {
+ const [icon, setIcon] = useState('')
+
+ useEffect(() => {
+ if (props.file && props.file.path) {
+ setIcon(getPathIcon(props.file.path))
+ }
+ }, [props.file])
+
+ return (
+ <>
+ {icon ? : null}
+
+ {path.basename(props.file.path)}
+ {path.dirname(props.file.path)}
+
+ >
+ )
+}
diff --git a/libs/remix-ui/search/src/lib/components/results/ResultItem.tsx b/libs/remix-ui/search/src/lib/components/results/ResultItem.tsx
new file mode 100644
index 0000000000..91bb40b51b
--- /dev/null
+++ b/libs/remix-ui/search/src/lib/components/results/ResultItem.tsx
@@ -0,0 +1,104 @@
+import React, { useContext, useEffect, useRef, useState } from 'react'
+import { SearchContext } from '../../context/context'
+import { SearchResult, SearchResultLine } from '../../types'
+import { ResultFileName } from './ResultFileName'
+import { ResultSummary } from './ResultSummary'
+
+interface ResultItemProps {
+ file: SearchResult
+}
+
+export const ResultItem = (props: ResultItemProps) => {
+ const { state, findText, disableForceReload, updateCount } = useContext(
+ SearchContext
+ )
+ const [loading, setLoading] = useState(false)
+ const [lines, setLines] = useState([])
+ const [toggleExpander, setToggleExpander] = useState(false)
+ const reloadTimeOut = useRef(null)
+ const subscribed = useRef(true)
+
+
+ useEffect(() => {
+ reload()
+ }, [props.file.timeStamp])
+
+ useEffect(() => {
+ if (props.file.forceReload) {
+ clearTimeout(reloadTimeOut.current)
+ reloadTimeOut.current = setTimeout(() => reload(), 1000)
+ }
+ }, [props.file.forceReload])
+
+ const toggleClass = () => {
+ setToggleExpander(!toggleExpander)
+ }
+
+ useEffect(() => {
+ reload()
+ }, [state.find])
+
+ useEffect(() => {
+ subscribed.current = true
+ return () => {
+ subscribed.current = false
+ }
+ }, [])
+
+ const reload = () => {
+ findText(props.file.filename).then(res => {
+ if (subscribed.current) {
+ setLines(res)
+ if (res) {
+ let count = 0
+ res.forEach(line => {
+ count += line.lines.length
+ })
+ updateCount(count, props.file.filename)
+ }
+ setLoading(false)
+ disableForceReload(props.file.filename)
+ }
+ })
+ }
+
+ return (
+ <>
+ {lines && lines.length ? (
+ <>
+
+
{' '}
+
+
+
+ {props.file.count}
+
+
+
+ {loading ? Loading...
: null}
+ {!toggleExpander && !loading ? (
+
+ {lines.map((line, index) => (
+
+ ))}
+
+ ) : 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
new file mode 100644
index 0000000000..157aa43006
--- /dev/null
+++ b/libs/remix-ui/search/src/lib/components/results/ResultSummary.tsx
@@ -0,0 +1,63 @@
+
+import { useDialogDispatchers } from '@remix-ui/app'
+import React, { useContext } from 'react'
+import { SearchContext } from '../../context/context'
+import { SearchResult, SearchResultLine, SearchResultLineLine } from '../../types'
+
+interface ResultSummaryProps {
+ searchResult: SearchResult
+ line: SearchResultLine
+ setLoading: (value: boolean) => void
+}
+
+export const ResultSummary = (props: ResultSummaryProps) => {
+ const { hightLightInPath, replaceText, state } = useContext(SearchContext)
+ const { modal } = useDialogDispatchers()
+ const selectLine = async (line: SearchResultLineLine) => {
+ await hightLightInPath(props.searchResult, line)
+ }
+
+ const confirmReplace = async (line: SearchResultLineLine) => {
+ props.setLoading(true)
+ try{
+ await replaceText(props.searchResult, line)
+ }catch(e){
+ props.setLoading(false)
+ }
+ }
+
+ const replace = async (line: SearchResultLineLine) => {
+ if(state.replaceWithOutConfirmation){
+ confirmReplace(line)
+ }else{
+ modal({ id: 'confirmreplace', title: 'Replace', message: `Are you sure you want to replace '${line.center}' by '${state.replace}' in ${props.searchResult.filename}?`, okLabel: 'Yes', okFn: confirmReplace, cancelLabel: 'No', cancelFn: ()=>{}, data: line })
+ }
+ }
+
+ return (
+ <>
+ {props.line.lines.map((lineItem, index) => (
+
+
{
+ selectLine(lineItem)
+ }}
+ data-id={`${props.searchResult.filename}-${lineItem.position.start.line}-${lineItem.position.start.column}`}
+ key={props.searchResult.filename}
+ className='search_plugin_search_line pb-1'
+ >
+
{lineItem.left.substring(lineItem.left.length - 20).trimStart()}
+
{lineItem.center}
+ {state.replace?
{state.replace}:<>>}
+
{lineItem.right.substring(0, 100)}
+
+
+
{
+ replace(lineItem)
+ }} className="codicon codicon-find-replace" role="button" aria-label="Replace" aria-disabled="false">
+
+
+ ))}
+ >
+ )
+}
diff --git a/libs/remix-ui/search/src/lib/components/results/Results.tsx b/libs/remix-ui/search/src/lib/components/results/Results.tsx
new file mode 100644
index 0000000000..b2094d23c9
--- /dev/null
+++ b/libs/remix-ui/search/src/lib/components/results/Results.tsx
@@ -0,0 +1,17 @@
+import React, { useContext, useEffect } from 'react'
+import { SearchContext } from '../../context/context'
+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.searchResults.map((result, index) => {
+ return
+ })}
+ {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/components/results/SearchHelper.ts b/libs/remix-ui/search/src/lib/components/results/SearchHelper.ts
new file mode 100644
index 0000000000..88ae6b1de1
--- /dev/null
+++ b/libs/remix-ui/search/src/lib/components/results/SearchHelper.ts
@@ -0,0 +1,108 @@
+import { EOL } from 'os'
+import { SearchResultLineLine } from '../../types'
+
+
+export const getDirectory = async (dir: string, plugin: any) => {
+ let result = []
+ const files = await plugin.call('fileManager', 'readdir', dir)
+ const fileArray = normalize(files)
+ for (const fi of fileArray) {
+ if (fi) {
+ const type = fi.data.isDirectory
+ if (type === true) {
+ result = [...result, ...(await getDirectory(`${fi.filename}`, plugin))]
+ } else {
+ result = [...result, fi.filename]
+ }
+ }
+ }
+ return result
+ }
+
+ const normalize = filesList => {
+ const folders = []
+ const files = []
+ Object.keys(filesList || {}).forEach(key => {
+ if (filesList[key].isDirectory) {
+ folders.push({
+ filename: key,
+ data: filesList[key]
+ })
+ } else {
+ files.push({
+ filename: key,
+ data: filesList[key]
+ })
+ }
+ })
+ return [...folders, ...files]
+ }
+
+export const findLinesInStringWithMatch = (str: string, re: RegExp) => {
+ return str
+ .split(/\r?\n/)
+ .map(function (line, i) {
+ const matchResult = matchesInString(line, re)
+ if (matchResult.length) {
+ return {
+ lines: splitLines(matchResult, i),
+ }
+ }
+ })
+ .filter(Boolean)
+}
+
+const matchesInString = (str: string, re: RegExp) => {
+ let a: RegExpExecArray
+ const results:RegExpExecArray[] = [];
+ while ((a = re.exec(str || '')) !== null) {
+ results.push(a);
+ }
+ return results
+}
+
+const splitLines = (matchResult: RegExpExecArray[], lineNumber: number) => {
+ return matchResult.map((matchResultPart, i) => {
+ const result:SearchResultLineLine = {
+ left: matchResultPart.input.substring(0, matchResultPart.index),
+ right: matchResultPart.input.substring(matchResultPart.index + matchResultPart[0].length),
+ center: matchResultPart[0],
+ position : {
+ start: {
+ line: lineNumber,
+ column: matchResultPart.index,
+ },
+ end: {
+ line: lineNumber,
+ column: matchResultPart.index + matchResultPart[0].length,
+ },
+ },
+ }
+ return result
+ })
+}
+
+function getEOL(text) {
+ const m = text.match(/\r\n|\n/g);
+ const u = m && m.filter(a => a === '\n').length;
+ const w = m && m.length - u;
+ if (u === w) {
+ return EOL; // use the OS default
+ }
+ return u > w ? '\n' : '\r\n';
+}
+
+
+export const replaceTextInLine = (str: string, searchResultLine: SearchResultLineLine, newText: string) => {
+ return str
+ .split(/\r?\n/)
+ .map(function (line, i) {
+ if (i === searchResultLine.position.start.line) {
+ return searchResultLine.left + newText + searchResultLine.right
+ }
+ return line
+ }).join(getEOL(str))
+}
+
+
+
diff --git a/libs/remix-ui/search/src/lib/context/context.tsx b/libs/remix-ui/search/src/lib/context/context.tsx
new file mode 100644
index 0000000000..e8ce76e287
--- /dev/null
+++ b/libs/remix-ui/search/src/lib/context/context.tsx
@@ -0,0 +1,239 @@
+import React, { useEffect, useRef } from 'react'
+import { createContext, useReducer } from 'react'
+import {
+ findLinesInStringWithMatch,
+ getDirectory,
+ replaceTextInLine
+} from '../components/results/SearchHelper'
+import { SearchReducer } from '../reducers/Reducer'
+import {
+ SearchState,
+ SearchResult,
+ SearchResultLine,
+ SearchResultLineLine,
+ SearchingInitialState
+} from '../types'
+import { filePathFilter } from '@jsdevtools/file-path-filter'
+import { escapeRegExp } from 'lodash'
+
+export interface SearchingStateInterface {
+ state: SearchState
+ setFind: (value: string) => void
+ setReplace: (value: string) => void
+ setInclude: (value: string) => void
+ setExclude: (value: string) => void
+ setCaseSensitive: (value: boolean) => void
+ setRegex: (value: boolean) => void
+ setWholeWord: (value: boolean) => void
+ setSearchResults: (value: SearchResult[]) => void
+ findText: (path: string) => Promise
+ hightLightInPath: (result: SearchResult, line: SearchResultLineLine) => void
+ replaceText: (result: SearchResult, line: SearchResultLineLine) => void
+ reloadFile: (file: string) => void
+ toggleCaseSensitive: () => void
+ toggleMatchWholeWord: () => void
+ toggleUseRegex: () => void
+ setReplaceWithoutConfirmation: (value: boolean) => void
+ disableForceReload: (file: string) => void
+ updateCount: (count: number, file: string) => void
+}
+
+export const SearchContext = createContext(null)
+
+export const SearchProvider = ({
+ children = [],
+ reducer = SearchReducer,
+ initialState = SearchingInitialState,
+ plugin = undefined
+} = {}) => {
+ const [state, dispatch] = useReducer(reducer, initialState)
+
+ const reloadTimeOut = useRef(null)
+ const value = {
+ state,
+ setFind: (value: string) => {
+ dispatch({
+ type: 'SET_FIND',
+ payload: value
+ })
+ },
+ setReplace: (value: string) => {
+ dispatch({
+ type: 'SET_REPLACE',
+ payload: value
+ })
+ },
+ setInclude: (value: string) => {
+ dispatch({
+ type: 'SET_INCLUDE',
+ payload: value
+ })
+ },
+ setExclude(value: string) {
+ dispatch({
+ type: 'SET_EXCLUDE',
+ payload: value
+ })
+ },
+ setCaseSensitive(value: boolean) {
+ dispatch({
+ type: 'SET_CASE_SENSITIVE',
+ payload: value
+ })
+ },
+ setWholeWord(value: boolean) {
+ dispatch({
+ type: 'SET_WHOLE_WORD',
+ payload: value
+ })
+ },
+ setRegex(value: boolean) {
+ dispatch({
+ type: 'SET_REGEX',
+ payload: value
+ })
+ },
+ setSearchResults(value: SearchResult[]) {
+ dispatch({
+ type: 'SET_SEARCH_RESULTS',
+ payload: value
+ })
+ },
+ reloadFile: async (file: string) => {
+ dispatch({
+ type: 'RELOAD_FILE',
+ payload: file
+ })
+ },
+ toggleUseRegex: () => {
+ dispatch({
+ type: 'TOGGLE_USE_REGEX',
+ payload: undefined
+ })
+ },
+ toggleCaseSensitive: () => {
+ dispatch({
+ type: 'TOGGLE_CASE_SENSITIVE',
+ payload: undefined
+ })
+ },
+ toggleMatchWholeWord: () => {
+ dispatch({
+ type: 'TOGGLE_MATCH_WHOLE_WORD',
+ payload: undefined
+ })
+ },
+ setReplaceWithoutConfirmation: (value: boolean) => {
+ dispatch({
+ type: 'SET_REPLACE_WITHOUT_CONFIRMATION',
+ payload: value
+ })
+ },
+ disableForceReload: (file: string) => {
+ dispatch({
+ type: 'DISABLE_FORCE_RELOAD',
+ payload: file
+ })
+ },
+ updateCount: (count: number, file: string) => {
+ dispatch({
+ type: 'UPDATE_COUNT',
+ payload: {count, file}
+ })
+ },
+ findText: async (path: string) => {
+ if (!plugin) return
+ try {
+ if (state.find.length < 3) 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)
+ return result
+ } catch (e) {}
+ },
+ hightLightInPath: async (
+ result: SearchResult,
+ line: SearchResultLineLine
+ ) => {
+ await plugin.call('editor', 'discardHighlight')
+ await plugin.call('editor', 'highlight', line.position, result.path)
+ },
+ replaceText: async (result: SearchResult, line: SearchResultLineLine) => {
+ try {
+ await plugin.call('editor', 'discardHighlight')
+ await plugin.call('editor', 'highlight', line.position, result.path)
+ const content = await plugin.call(
+ 'fileManager',
+ 'readFile',
+ result.path
+ )
+
+ await plugin.call(
+ 'fileManager',
+ 'setFile',
+ result.path,
+ replaceTextInLine(content, line, state.replace)
+ )
+ } catch (e) {
+ throw new Error(e)
+ }
+ }
+ }
+
+ const reloadStateForFile = async (file: string) => {
+ await value.reloadFile(file)
+ }
+
+ useEffect(() => {
+ plugin.on('filePanel', 'setWorkspace', () => {
+ value.setSearchResults(null)
+ })
+ plugin.on('fileManager', 'fileSaved', async file => {
+ await reloadStateForFile(file)
+ })
+ return () => {
+ plugin.off('fileManager', 'fileChanged')
+ plugin.off('filePanel', 'setWorkspace')
+ }
+ }, [])
+
+ useEffect(() => {
+ if (state.find) {
+ (async () => {
+ const files = await getDirectory('/', plugin)
+ const pathFilter: any = {}
+ if (state.include){
+ const includeWithGlobalExpression = state.include.replaceAll(/(? i.trim())
+ }
+ if (state.exclude){
+ const excludeWithGlobalExpression = state.exclude.replaceAll(/(? i.trim())
+ }
+ const filteredFiles = files.filter(filePathFilter(pathFilter)).map(file => {
+ const r: SearchResult = {
+ filename: file,
+ lines: [],
+ path: file,
+ timeStamp: Date.now(),
+ forceReload: false,
+ count: 0
+ }
+ return r
+ })
+ value.setSearchResults(filteredFiles)
+ })()
+ }
+ }, [state.timeStamp])
+
+ return (
+ <>
+ {children}
+ >
+ )
+}
diff --git a/libs/remix-ui/search/src/lib/reducers/Reducer.ts b/libs/remix-ui/search/src/lib/reducers/Reducer.ts
new file mode 100644
index 0000000000..05e9efaecb
--- /dev/null
+++ b/libs/remix-ui/search/src/lib/reducers/Reducer.ts
@@ -0,0 +1,101 @@
+import { Action, SearchingInitialState, SearchState } from "../types"
+
+export const SearchReducer = (state: SearchState = SearchingInitialState, action: Action) => {
+ switch (action.type) {
+ case 'SET_FIND':
+ return {
+ ...state,
+ find: action.payload,
+ timeStamp: Date.now()
+ }
+
+ case 'SET_REPLACE':
+ return {
+ ...state,
+ replace: action.payload,
+ }
+
+ case 'SET_INCLUDE':
+ return {
+ ...state,
+ include: action.payload,
+ timeStamp: Date.now()
+ }
+
+ case 'SET_EXCLUDE':
+ return {
+ ...state,
+ exclude: action.payload,
+ timeStamp: Date.now()
+ }
+
+ case 'SET_SEARCH_RESULTS':
+ return {
+ ...state,
+ searchResults: action.payload,
+ count: 0
+ }
+ case 'UPDATE_COUNT':
+ if (state.searchResults) {
+ const findFile = state.searchResults.find(file => file.filename === action.payload.file)
+ let count = 0
+ if (findFile) {
+ findFile.count = action.payload.count
+ }
+ state.searchResults.forEach(file => {
+ if (file.count) {
+ count += file.count
+ }
+ })
+ return {
+ ...state,
+ count: count
+ }
+ } else {
+ return state
+ }
+ case 'TOGGLE_CASE_SENSITIVE':
+ return {
+ ...state,
+ casesensitive: !state.casesensitive,
+ timeStamp: Date.now()
+ }
+ case 'TOGGLE_USE_REGEX':
+ return {
+ ...state,
+ useRegExp: !state.useRegExp,
+ timeStamp: Date.now()
+ }
+ case 'TOGGLE_MATCH_WHOLE_WORD':
+ return {
+ ...state,
+ matchWord: !state.matchWord,
+ timeStamp: Date.now()
+ }
+ case 'SET_REPLACE_WITHOUT_CONFIRMATION':
+ return {
+ ...state,
+ replaceWithOutConfirmation: action.payload,
+ }
+ case 'DISABLE_FORCE_RELOAD':
+ if (state.searchResults) {
+ const findFile = state.searchResults.find(file => file.filename === action.payload)
+ if (findFile) findFile.forceReload = false
+ }
+ return {
+ ...state,
+ }
+ case 'RELOAD_FILE':
+ if (state.searchResults) {
+ const findFile = state.searchResults.find(file => file.filename === action.payload)
+ if (findFile) findFile.forceReload = true
+ }
+ return {
+ ...state,
+ }
+ default:
+ return {
+ ...state,
+ }
+ }
+}
\ No newline at end of file
diff --git a/libs/remix-ui/search/src/lib/search.css b/libs/remix-ui/search/src/lib/search.css
new file mode 100644
index 0000000000..895ce83e7f
--- /dev/null
+++ b/libs/remix-ui/search/src/lib/search.css
@@ -0,0 +1,106 @@
+.search_plugin_search_result_item_title {
+ display: flex;
+ -webkit-user-select: none; /* Safari */
+ -moz-user-select: none; /* Firefox */
+ -ms-user-select: none; /* IE10+/Edge */
+ user-select: none; /* Standard */
+ cursor: pointer;
+ align-items: center;
+}
+
+.search_plugin_wrap_summary {
+ overflow: hidden;
+ white-space: nowrap;
+ -webkit-user-select: none; /* Safari */
+ -moz-user-select: none; /* Firefox */
+ -ms-user-select: none; /* IE10+/Edge */
+ user-select: none; /* Standard */
+ cursor: pointer;
+}
+
+.search_plugin_find-part {
+ display: flex;
+ flex-direction: column;
+ padding: 2px;
+}
+
+.search_plugin_controls {
+ display: flex;
+}
+
+.search_plugin_search_tab .search_plugin_search_line_container {
+ display: flex;
+ flex-direction: row;
+ position: relative;
+}
+
+.search_plugin_search_tab .search_plugin_search_line {
+ width: 100%;
+ overflow: hidden;
+ display: flex;
+}
+
+.search_plugin_search_tab .search_plugin_search_control {
+ flex-grow: 0;
+ position: absolute;
+ right: 0px;
+ top: 0px;
+}
+
+.search_plugin_summary_right {
+ min-width: 0;
+ white-space: pre;
+ text-overflow: ellipsis;
+ overflow: hidden;
+}
+
+.search_plugin_search_tab .search_plugin_replace_strike {
+ text-decoration: line-through;
+}
+
+.search_plugin_summary_left {
+ white-space: pre;
+}
+
+.search_plugin_search_tab mark {
+ padding: 0;
+}
+
+.search_plugin_search_tab .search_plugin_search_line_container .search_plugin_search_control {
+ display: none;
+}
+
+.search_plugin_search_tab .search_plugin_search_line_container:hover .search_plugin_search_control {
+ display: block;
+}
+
+.search_plugin_search_tab .search_plugin_search_line_container:hover .search_plugin_search_line {
+ width: 93%;
+}
+
+.search_plugin_search-input {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+}
+
+.search_plugin_search_tab .checked {
+ background-color: var(--secondary);
+}
+
+.search_plugin_search_tab .search_plugin_search_file_name {
+ text-overflow: ellipsis;
+ overflow: hidden;
+ text-transform: uppercase;
+}
+
+.search_plugin_search_tab .search_plugin_result_count {
+ flex-grow: 1;
+ text-align: right;
+ display: flex;
+ justify-content: flex-end;
+}
+
+.search_plugin_search_tab .search_plugin_result_count_number {
+ font-size: x-small;
+}
\ 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
new file mode 100644
index 0000000000..af76487c51
--- /dev/null
+++ b/libs/remix-ui/search/src/lib/types/index.ts
@@ -0,0 +1,66 @@
+import { count } from "console";
+
+export interface Action {
+ type: string
+ payload: any
+}
+
+interface position {
+ start: {
+ line: number
+ column: number
+ },
+ end: {
+ line: number
+ column: number
+ }
+}
+
+export interface SearchResultLineLine {
+ left: any,
+ center: any,
+ right: any,
+ position: position
+}
+export interface SearchResultLine {
+ lines: SearchResultLineLine[]
+}
+
+export interface SearchResult {
+ filename: string,
+ path: string,
+ lines: SearchResultLine[],
+ timeStamp: number,
+ forceReload: boolean,
+ count: number
+}
+
+export interface SearchState {
+ find: string,
+ searchResults: SearchResult[],
+ replace: string,
+ include: string,
+ exclude: string,
+ casesensitive: boolean,
+ matchWord: boolean,
+ replaceWithOutConfirmation: boolean,
+ useRegExp: boolean,
+ timeStamp: number,
+ count: number,
+ maxResults: number
+}
+
+export const SearchingInitialState: SearchState = {
+ find: '',
+ replace: '',
+ include: '',
+ exclude: '',
+ searchResults: [],
+ casesensitive: false,
+ matchWord: false,
+ useRegExp: false,
+ replaceWithOutConfirmation: false,
+ timeStamp: 0,
+ count: 0,
+ maxResults: 500
+}
\ No newline at end of file
diff --git a/libs/remix-ui/search/tsconfig.json b/libs/remix-ui/search/tsconfig.json
new file mode 100644
index 0000000000..a7180ef589
--- /dev/null
+++ b/libs/remix-ui/search/tsconfig.json
@@ -0,0 +1,19 @@
+{
+ "extends": "../../../tsconfig.base.json",
+ "compilerOptions": {
+ "jsx": "react",
+ "allowJs": true,
+ "esModuleInterop": true,
+ "allowSyntheticDefaultImports": true
+ },
+ "files": [],
+ "include": [],
+ "references": [
+ {
+ "path": "./tsconfig.lib.json"
+ },
+ {
+ "path": "./tsconfig.spec.json"
+ }
+ ]
+}
diff --git a/libs/remix-ui/search/tsconfig.lib.json b/libs/remix-ui/search/tsconfig.lib.json
new file mode 100644
index 0000000000..b560bc4dec
--- /dev/null
+++ b/libs/remix-ui/search/tsconfig.lib.json
@@ -0,0 +1,13 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../../dist/out-tsc",
+ "types": ["node"]
+ },
+ "files": [
+ "../../../node_modules/@nrwl/react/typings/cssmodule.d.ts",
+ "../../../node_modules/@nrwl/react/typings/image.d.ts"
+ ],
+ "exclude": ["**/*.spec.ts", "**/*.spec.tsx"],
+ "include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"]
+}
diff --git a/nx.json b/nx.json
index 7f7de57643..bbf5b08f6f 100644
--- a/nx.json
+++ b/nx.json
@@ -139,6 +139,9 @@
"remix-ui-vertical-icons-panel": {
"tags": []
},
+ "remix-ui-search": {
+ "tags": []
+ },
"remix-ui-home-tab": {
"tags": []
},
diff --git a/package.json b/package.json
index 72cc1f3a68..f34cf7859c 100644
--- a/package.json
+++ b/package.json
@@ -45,7 +45,7 @@
"workspace-schematic": "nx workspace-schematic",
"dep-graph": "nx dep-graph",
"help": "nx help",
- "lint:libs": "nx run-many --target=lint --projects=remix-analyzer,remix-astwalker,remix-debug,remix-lib,remix-simulator,remix-solidity,remix-tests,remix-url-resolver,remixd,remix-ui-tree-view,remix-ui-modal-dialog,remix-ui-toaster,remix-ui-helper,remix-ui-debugger-ui,remix-ui-workspace,remix-ui-static-analyser,remix-ui-checkbox,remix-ui-settings,remix-core-plugin,remix-ui-renderer,remix-ui-publish-to-storage,remix-ui-solidity-compiler,solidity-unit-testing,remix-ui-plugin-manager,remix-ui-terminal,remix-ui-editor,remix-ui-app,remix-ui-tabs,remix-ui-panel,remix-ui-run-tab,remix-ui-permission-handler",
+ "lint:libs": "nx run-many --target=lint --projects=remix-analyzer,remix-astwalker,remix-debug,remix-lib,remix-simulator,remix-solidity,remix-tests,remix-url-resolver,remixd,remix-ui-tree-view,remix-ui-modal-dialog,remix-ui-toaster,remix-ui-helper,remix-ui-debugger-ui,remix-ui-workspace,remix-ui-static-analyser,remix-ui-checkbox,remix-ui-settings,remix-core-plugin,remix-ui-renderer,remix-ui-publish-to-storage,remix-ui-solidity-compiler,solidity-unit-testing,remix-ui-plugin-manager,remix-ui-terminal,remix-ui-editor,remix-ui-app,remix-ui-tabs,remix-ui-panel,remix-ui-run-tab,remix-ui-permission-handler,remix-ui-search",
"build:libs": "nx run-many --target=build --parallel=false --with-deps=true --projects=remix-analyzer,remix-astwalker,remix-debug,remix-lib,remix-simulator,remix-solidity,remix-tests,remix-url-resolver,remixd",
"test:libs": "nx run-many --target=test --projects=remix-analyzer,remix-astwalker,remix-debug,remix-lib,remix-simulator,remix-solidity,remix-tests,remix-url-resolver,remixd",
"publish:libs": "npm run build:libs && lerna publish --skip-git && npm run bumpVersion:libs",
@@ -97,6 +97,7 @@
"nightwatch_local_pluginApi": "npm run build:e2e && nightwatch --config dist/apps/remix-ide-e2e/nightwatch.js dist/apps/remix-ide-e2e/src/tests/plugin_api_*.js --env=chrome",
"nightwatch_local_migrate_filesystem": "npm run build:e2e && nightwatch --config dist/apps/remix-ide-e2e/nightwatch.js dist/apps/remix-ide-e2e/src/tests/migrateFileSystem.test.js --env=chrome",
"nightwatch_local_stress_editor": "npm run build:e2e && nightwatch --config dist/apps/remix-ide-e2e/nightwatch.js dist/apps/remix-ide-e2e/src/tests/stress.editor.js --env=chromeDesktop",
+ "nightwatch_local_search": "npm run build:e2e && nightwatch --config dist/apps/remix-ide-e2e/nightwatch.js dist/apps/remix-ide-e2e/src/tests/search.test.js --env=chromeDesktop",
"onchange": "onchange apps/remix-ide/build/app.js -- npm-run-all lint",
"remixd": "nx build remixd && chmod +x dist/libs/remixd/src/bin/remixd.js && dist/libs/remixd/src/bin/remixd.js -s ./apps/remix-ide/contracts --remix-ide http://127.0.0.1:8080",
"selenium": "selenium-standalone start",
@@ -172,6 +173,7 @@
"ethers": "^5.4.2",
"ethjs-util": "^0.1.6",
"express-ws": "^4.0.0",
+ "file-path-filter": "^3.0.2",
"file-saver": "^2.0.5",
"form-data": "^4.0.0",
"fs-extra": "^3.0.1",
diff --git a/tsconfig.base.json b/tsconfig.base.json
index 8105a813ef..d9e148acdb 100644
--- a/tsconfig.base.json
+++ b/tsconfig.base.json
@@ -40,6 +40,7 @@
],
"@remix-project/remixd": ["dist/libs/remixd/index.js"],
"@remix-ui/tree-view": ["libs/remix-ui/tree-view/src/index.ts"],
+ "@remix-ui/search": ["libs/remix-ui/search/src/index.ts"],
"@remix-ui/debugger-ui": ["libs/remix-ui/debugger-ui/src/index.ts"],
"@remix-ui/utils": ["libs/remix-ui/utils/src/index.ts"],
"@remix-ui/clipboard": ["libs/remix-ui/clipboard/src/index.ts"],
diff --git a/workspace.json b/workspace.json
index dc00725dc8..98dcecd3ff 100644
--- a/workspace.json
+++ b/workspace.json
@@ -1108,6 +1108,21 @@
}
}
},
+ "remix-ui-search": {
+ "root": "libs/remix-ui/search",
+ "sourceRoot": "libs/remix-ui/search/src",
+ "projectType": "library",
+ "architect": {
+ "lint": {
+ "builder": "@nrwl/linter:lint",
+ "options": {
+ "linter": "eslint",
+ "tsConfig": ["libs/remix-ui/search/tsconfig.lib.json"],
+ "exclude": ["**/node_modules/**", "!libs/remix-ui/search/**/*"]
+ }
+ }
+ }
+ },
"remix-ui-panel": {
"root": "libs/remix-ui/panel",
"sourceRoot": "libs/remix-ui/panel/src",