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 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + 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",