commit
7e0ef62be2
@ -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) |
||||
}) |
||||
} |
||||
} |
@ -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 ( |
||||
<div id='searchTab'> |
||||
<SearchTab plugin={this}></SearchTab> |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
} |
After Width: | Height: | Size: 2.6 KiB |
@ -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' |
||||
|
@ -0,0 +1,4 @@ |
||||
{ |
||||
"presets": ["@nrwl/react/babel"], |
||||
"plugins": [] |
||||
} |
@ -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" |
||||
} |
||||
} |
@ -0,0 +1,5 @@ |
||||
{ |
||||
"tabWidth": 2, |
||||
"singleQuote": true, |
||||
"semi": false |
||||
} |
@ -0,0 +1 @@ |
||||
export { SearchTab } from './lib/components/Search'; |
@ -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<string>('.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 ( |
||||
<> |
||||
<div className="search_plugin_find-part"> |
||||
<label>exclude</label> |
||||
<input |
||||
id='search_exclude' |
||||
placeholder="Exclude ie .git/**/*" |
||||
className="form-control" |
||||
onChange={change} |
||||
value={excludeInput} |
||||
></input> |
||||
</div> |
||||
</> |
||||
) |
||||
} |
@ -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 ( |
||||
<> |
||||
<div className="search_plugin_find-part"> |
||||
<label>search</label> |
||||
<div className="search_plugin_search-input"> |
||||
<input |
||||
id='search_input' |
||||
placeholder="Search" |
||||
className="form-control" |
||||
onChange={change} |
||||
></input> |
||||
<div className="search_plugin_controls"> |
||||
<div |
||||
data-id='search_case_sensitive' |
||||
title="Match Case" |
||||
className={`monaco-custom-checkbox codicon codicon-case-sensitive ${ |
||||
state.casesensitive ? 'checked' : '' |
||||
}`}
|
||||
role="checkbox" |
||||
aria-checked="false" |
||||
aria-label="Match Case" |
||||
aria-disabled="false" |
||||
onClick={() => { |
||||
toggleCaseSensitive() |
||||
}} |
||||
></div> |
||||
<div |
||||
data-id='search_whole_word' |
||||
title="Match Whole Word" |
||||
className={`monaco-custom-checkbox codicon codicon-whole-word ${ |
||||
state.matchWord ? 'checked' : '' |
||||
}`}
|
||||
role="checkbox" |
||||
aria-checked="false" |
||||
aria-label="Match Whole Word" |
||||
aria-disabled="false" |
||||
onClick={() => { |
||||
toggleMatchWholeWord() |
||||
}} |
||||
></div> |
||||
<div |
||||
data-id='search_use_regex' |
||||
title="Use Regular Expression" |
||||
className={`monaco-custom-checkbox codicon codicon-regex ${ |
||||
state.useRegExp ? 'checked' : '' |
||||
}`}
|
||||
role="checkbox" |
||||
aria-checked="false" |
||||
aria-label="Use Regular Expression" |
||||
aria-disabled="false" |
||||
onClick={() => { |
||||
toggleUseRegex() |
||||
}} |
||||
></div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</> |
||||
) |
||||
} |
@ -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<string>('') |
||||
const timeOutId = useRef(null) |
||||
const change = e => { |
||||
setIncludeInput(e.target.value) |
||||
clearTimeout(timeOutId.current) |
||||
timeOutId.current = setTimeout(() => setInclude(e.target.value), 500) |
||||
} |
||||
|
||||
return ( |
||||
<> |
||||
<div className="search_plugin_find-part"> |
||||
<label>include</label> |
||||
<input |
||||
id='search_include' |
||||
placeholder="Include ie contracts/**/*.sol" |
||||
className="form-control" |
||||
onChange={change} |
||||
value={includeInput} |
||||
></input> |
||||
</div> |
||||
</> |
||||
) |
||||
} |
@ -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 ( |
||||
<> |
||||
<div className="search_plugin_find-part"> |
||||
<div className="mb-2 remixui_nightlyBuilds custom-control custom-checkbox"> |
||||
<input |
||||
className="mr-2 custom-control-input" |
||||
id="confirm_replace" |
||||
type="checkbox" |
||||
onChange={change} |
||||
/> |
||||
<label |
||||
htmlFor='confirm_replace' |
||||
data-id="confirm_replace_label" |
||||
className="form-check-label custom-control-label" |
||||
> |
||||
replace without confirmation |
||||
</label> |
||||
</div> |
||||
</div> |
||||
</> |
||||
) |
||||
} |
@ -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 ( |
||||
<> |
||||
<div className="search_plugin_find-part"> |
||||
<label>replace</label> |
||||
<input |
||||
id='search_replace' |
||||
placeholder="Replace" |
||||
className="form-control" |
||||
onChange={change} |
||||
></input> |
||||
</div> |
||||
</> |
||||
) |
||||
} |
@ -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 ( |
||||
<> |
||||
<div className="search_plugin_search_tab pl-2 pr-2"> |
||||
<SearchProvider plugin={plugin}> |
||||
<Find></Find> |
||||
<Replace></Replace> |
||||
<Include></Include> |
||||
<Exclude></Exclude> |
||||
<OverWriteCheck></OverWriteCheck> |
||||
<Results></Results> |
||||
</SearchProvider> |
||||
</div> |
||||
</> |
||||
) |
||||
} |
@ -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<string>('') |
||||
|
||||
useEffect(() => { |
||||
if (props.file && props.file.path) { |
||||
setIcon(getPathIcon(props.file.path)) |
||||
} |
||||
}, [props.file]) |
||||
|
||||
return ( |
||||
<> |
||||
{icon ? <div className={`${icon} caret caret_tv`}></div> : null} |
||||
<div title={props.file.filename} className="search_plugin_search_file_name ml-2"> |
||||
{path.basename(props.file.path)} |
||||
<span className='pl-1 text-muted text-lowercase'>{path.dirname(props.file.path)}</span> |
||||
</div> |
||||
</> |
||||
) |
||||
} |
@ -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<boolean>(false) |
||||
const [lines, setLines] = useState<SearchResultLine[]>([]) |
||||
const [toggleExpander, setToggleExpander] = useState<boolean>(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 ? ( |
||||
<> |
||||
<div onClick={toggleClass} className="search_plugin_search_result_item_title"> |
||||
<button className="btn"> |
||||
<i |
||||
className={`fas ${ |
||||
toggleExpander ? 'fa-angle-right' : 'fa-angle-down' |
||||
}`}
|
||||
aria-hidden="true" |
||||
></i> |
||||
</button>{' '} |
||||
<ResultFileName file={props.file} /> |
||||
<div className="search_plugin_result_count"> |
||||
<div className="search_plugin_result_count_number badge badge-pill badge-secondary"> |
||||
{props.file.count} |
||||
</div> |
||||
</div> |
||||
</div> |
||||
{loading ? <div className="loading">Loading...</div> : null} |
||||
{!toggleExpander && !loading ? ( |
||||
<div className="p-1 search_plugin_wrap_summary"> |
||||
{lines.map((line, index) => ( |
||||
<ResultSummary |
||||
setLoading={setLoading} |
||||
key={index} |
||||
searchResult={props.file} |
||||
line={line} |
||||
/> |
||||
))} |
||||
</div> |
||||
) : null} |
||||
</> |
||||
) : ( |
||||
<></> |
||||
)} |
||||
</> |
||||
) |
||||
} |
@ -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) => ( |
||||
<div className='search_plugin_search_line_container' key={index}> |
||||
<div |
||||
onClick={async () => { |
||||
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' |
||||
> |
||||
<div className='search_plugin_summary_left'>{lineItem.left.substring(lineItem.left.length - 20).trimStart()}</div> |
||||
<mark className={`search_plugin_summary_center ${state.replace? 'search_plugin_replace_strike':''}`}>{lineItem.center}</mark> |
||||
{state.replace? <mark className='search_plugin_replacement'>{state.replace}</mark>:<></>} |
||||
<div className='search_plugin_summary_right'>{lineItem.right.substring(0, 100)}</div> |
||||
</div> |
||||
<div className='search_plugin_search_control'> |
||||
<div title="Replace" data-id={`replace-${props.searchResult.filename}-${lineItem.position.start.line}-${lineItem.position.start.column}`} onClick={async () => { |
||||
replace(lineItem) |
||||
}} className="codicon codicon-find-replace" role="button" aria-label="Replace" aria-disabled="false"></div> |
||||
</div> |
||||
</div> |
||||
))} |
||||
</> |
||||
) |
||||
} |
@ -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 ( |
||||
<div data-id='search_results'> |
||||
{state.find ? <div className='search_plugin_result_count_number badge badge-pill badge-secondary'>{state.count} results</div>: null} |
||||
{state.count < state.maxResults && state.searchResults && |
||||
state.searchResults.map((result, index) => { |
||||
return <ResultItem key={index} file={result} /> |
||||
})} |
||||
{state.find && state.count >= state.maxResults? <div className='alert alert-warning mt-1'>Too many results to display.<br></br>Please narrow your search.</div>: null} |
||||
</div> |
||||
) |
||||
} |
@ -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)) |
||||
} |
||||
|
||||
|
||||
|
@ -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<SearchResultLine[]> |
||||
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<SearchingStateInterface>(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(/(?<!\/)(\*\.)/g, '**/*.') |
||||
pathFilter.include = includeWithGlobalExpression.split(',').map(i => i.trim()) |
||||
} |
||||
if (state.exclude){ |
||||
const excludeWithGlobalExpression = state.exclude.replaceAll(/(?<!\/)(\*\.)/g, '**/*.') |
||||
pathFilter.exclude = excludeWithGlobalExpression.split(',').map(i => 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 ( |
||||
<> |
||||
<SearchContext.Provider value={value}>{children}</SearchContext.Provider> |
||||
</> |
||||
) |
||||
} |
@ -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, |
||||
} |
||||
} |
||||
} |
@ -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; |
||||
} |
@ -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 |
||||
} |
@ -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" |
||||
} |
||||
] |
||||
} |
@ -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"] |
||||
} |
Loading…
Reference in new issue