pull/2170/head
bunsenstraat 3 years ago committed by filip mertens
parent 1fd9708fe4
commit 71c24b0da7
  1. 2
      libs/remix-ui/search/src/lib/components/Exclude.tsx
  2. 2
      libs/remix-ui/search/src/lib/components/Include.tsx
  3. 2
      libs/remix-ui/search/src/lib/components/Replace.tsx
  4. 2
      libs/remix-ui/search/src/lib/components/Search.tsx
  5. 29
      libs/remix-ui/search/src/lib/components/Undo.tsx
  6. 26
      libs/remix-ui/search/src/lib/components/results/ResultItem.tsx
  7. 4
      libs/remix-ui/search/src/lib/components/results/Results.tsx
  8. 3
      libs/remix-ui/search/src/lib/components/results/SearchHelper.ts
  9. 114
      libs/remix-ui/search/src/lib/context/context.tsx
  10. 37
      libs/remix-ui/search/src/lib/reducers/Reducer.ts
  11. 20
      libs/remix-ui/search/src/lib/types/index.ts

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

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

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

@ -8,6 +8,7 @@ import { Exclude } from './Exclude'
import { Replace } from './Replace' import { Replace } from './Replace'
import { OverWriteCheck } from './OverWriteCheck' import { OverWriteCheck } from './OverWriteCheck'
import { FindContainer } from './FindContainer' import { FindContainer } from './FindContainer'
import { Undo } from './Undo'
export const SearchTab = props => { export const SearchTab = props => {
@ -21,6 +22,7 @@ return (
<Include></Include> <Include></Include>
<Exclude></Exclude> <Exclude></Exclude>
<OverWriteCheck></OverWriteCheck> <OverWriteCheck></OverWriteCheck>
<Undo></Undo>
<Results></Results> <Results></Results>
</SearchProvider> </SearchProvider>
</div> </div>

@ -0,0 +1,29 @@
import { useDialogDispatchers } from "@remix-ui/app"
import React from "react"
import { useContext } from "react"
import { SearchContext } from "../context/context"
export const Undo = () => {
const {
state,
undoReplace
} = useContext(SearchContext)
const { alert } = useDialogDispatchers()
const undo = async () => {
try{
await undoReplace(state.undoBuffer[0])
}catch(e){
alert({ id: 'undo_error', title: 'Cannot undo this change', message: e.message })
}
}
return (<>
{state.undoBuffer && state.undoBuffer.length > 0 ?
<button onClick={async() => await undo()} className="btn btn-primary btn-block">
<div className="fas fa-undo mr-2"></div>
Undo changes to {state.undoBuffer[0].path}
</button> : null}
</>)
}

@ -1,3 +1,4 @@
import { useDialogDispatchers } from '@remix-ui/app'
import React, { useContext, useEffect, useRef, useState } from 'react' import React, { useContext, useEffect, useRef, useState } from 'react'
import { SearchContext } from '../../context/context' import { SearchContext } from '../../context/context'
import { SearchResult, SearchResultLine } from '../../types' import { SearchResult, SearchResultLine } from '../../types'
@ -9,7 +10,7 @@ interface ResultItemProps {
} }
export const ResultItem = (props: ResultItemProps) => { export const ResultItem = (props: ResultItemProps) => {
const { state, findText, disableForceReload, updateCount } = useContext( const { state, findText, disableForceReload, updateCount, replaceAllInFile } = useContext(
SearchContext SearchContext
) )
const [loading, setLoading] = useState<boolean>(false) const [loading, setLoading] = useState<boolean>(false)
@ -17,7 +18,7 @@ export const ResultItem = (props: ResultItemProps) => {
const [toggleExpander, setToggleExpander] = useState<boolean>(false) const [toggleExpander, setToggleExpander] = useState<boolean>(false)
const reloadTimeOut = useRef(null) const reloadTimeOut = useRef(null)
const subscribed = useRef(true) const subscribed = useRef(true)
const { modal } = useDialogDispatchers()
useEffect(() => { useEffect(() => {
reload() reload()
@ -41,10 +42,28 @@ export const ResultItem = (props: ResultItemProps) => {
useEffect(() => { useEffect(() => {
subscribed.current = true subscribed.current = true
return () => { return () => {
updateCount(0, props.file.filename)
subscribed.current = false subscribed.current = false
} }
}, []) }, [])
const confirmReplace = async () => {
setLoading(true)
try {
await replaceAllInFile(props.file)
} catch (e) {
}
setLoading(false)
}
const replace = async () => {
if(state.replaceWithOutConfirmation){
confirmReplace()
}else{
modal({ id: 'confirmreplace', title: 'Replace', message: `Are you sure you want to replace '${state.find}' by '${state.replace}' in ${props.file.filename}?`, okLabel: 'Yes', okFn: confirmReplace, cancelLabel: 'No', cancelFn: ()=>{}, data: null })
}
}
const reload = () => { const reload = () => {
findText(props.file.filename).then(res => { findText(props.file.filename).then(res => {
if (subscribed.current) { if (subscribed.current) {
@ -85,7 +104,8 @@ export const ResultItem = (props: ResultItemProps) => {
{loading ? <div className="loading">Loading...</div> : null} {loading ? <div className="loading">Loading...</div> : null}
{!toggleExpander && !loading ? ( {!toggleExpander && !loading ? (
<div className="p-1 search_plugin_wrap_summary"> <div className="p-1 search_plugin_wrap_summary">
{lines.map((line, index) => ( {state.replaceEnabled? <div onClick={async() => replace()} className='btn btn-primary btn-block mb-2 btn-sm'>Replace all</div>:null}
{lines.map((line, index) => (
index < state.maxLines ? index < state.maxLines ?
<ResultSummary <ResultSummary
setLoading={setLoading} setLoading={setLoading}

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

@ -92,6 +92,9 @@ function getEOL(text) {
return u > w ? '\n' : '\r\n'; return u > w ? '\n' : '\r\n';
} }
export const replaceAllInFile = (string: string, re:RegExp, newText: string) => {
return string.replace(re, newText)
}
export const replaceTextInLine = (str: string, searchResultLine: SearchResultLineLine, newText: string) => { export const replaceTextInLine = (str: string, searchResultLine: SearchResultLineLine, newText: string) => {
return str return str

@ -3,6 +3,7 @@ import { createContext, useReducer } from 'react'
import { import {
findLinesInStringWithMatch, findLinesInStringWithMatch,
getDirectory, getDirectory,
replaceAllInFile,
replaceTextInLine replaceTextInLine
} from '../components/results/SearchHelper' } from '../components/results/SearchHelper'
import { SearchReducer } from '../reducers/Reducer' import { SearchReducer } from '../reducers/Reducer'
@ -11,7 +12,8 @@ import {
SearchResult, SearchResult,
SearchResultLine, SearchResultLine,
SearchResultLineLine, SearchResultLineLine,
SearchingInitialState SearchingInitialState,
undoBufferRecord
} from '../types' } from '../types'
import { filePathFilter } from '@jsdevtools/file-path-filter' import { filePathFilter } from '@jsdevtools/file-path-filter'
import { escapeRegExp } from 'lodash' import { escapeRegExp } from 'lodash'
@ -29,7 +31,7 @@ export interface SearchingStateInterface {
setSearchResults: (value: SearchResult[]) => void setSearchResults: (value: SearchResult[]) => void
findText: (path: string) => Promise<SearchResultLine[]> findText: (path: string) => Promise<SearchResultLine[]>
hightLightInPath: (result: SearchResult, line: SearchResultLineLine) => void hightLightInPath: (result: SearchResult, line: SearchResultLineLine) => void
replaceText: (result: SearchResult, line: SearchResultLineLine) => void replaceText: (result: SearchResult, line: SearchResultLineLine) => Promise<void>
reloadFile: (file: string) => void reloadFile: (file: string) => void
toggleCaseSensitive: () => void toggleCaseSensitive: () => void
toggleMatchWholeWord: () => void toggleMatchWholeWord: () => void
@ -37,6 +39,9 @@ export interface SearchingStateInterface {
setReplaceWithoutConfirmation: (value: boolean) => void setReplaceWithoutConfirmation: (value: boolean) => void
disableForceReload: (file: string) => void disableForceReload: (file: string) => void
updateCount: (count: number, file: string) => void updateCount: (count: number, file: string) => void
replaceAllInFile: (result: SearchResult) => Promise<void>
undoReplace: (buffer: undoBufferRecord) => Promise<void>
clearUndo: () => void
} }
export const SearchContext = createContext<SearchingStateInterface>(null) export const SearchContext = createContext<SearchingStateInterface>(null)
@ -145,7 +150,7 @@ export const SearchProvider = ({
updateCount: (count: number, file: string) => { updateCount: (count: number, file: string) => {
dispatch({ dispatch({
type: 'UPDATE_COUNT', type: 'UPDATE_COUNT',
payload: {count, file} payload: { count, file }
}) })
}, },
findText: async (path: string) => { findText: async (path: string) => {
@ -153,15 +158,9 @@ export const SearchProvider = ({
try { try {
if (state.find.length < 1) return if (state.find.length < 1) return
const text = await plugin.call('fileManager', 'readFile', path) const text = await plugin.call('fileManager', 'readFile', path)
let flags = 'g' const result: SearchResultLine[] = findLinesInStringWithMatch(text, createRegExFromFind())
let find = state.find
if (!state.casesensitive) flags += 'i'
if (!state.useRegExp) find = escapeRegExp(find)
if (state.matchWord) find = `\\b${find}\\b`
const re = new RegExp(find, flags)
const result: SearchResultLine[] = findLinesInStringWithMatch(text, re)
return result return result
} catch (e) {} } catch (e) { }
}, },
hightLightInPath: async ( hightLightInPath: async (
result: SearchResult, result: SearchResult,
@ -180,26 +179,81 @@ export const SearchProvider = ({
'readFile', 'readFile',
result.path result.path
) )
const replaced = replaceTextInLine(content, line, state.replace)
await plugin.call( await plugin.call(
'fileManager', 'fileManager',
'setFile', 'setFile',
result.path, result.path,
replaceTextInLine(content, line, state.replace) replaced
) )
setUndoState(content, replaced, result.path)
} catch (e) { } catch (e) {
throw new Error(e) throw new Error(e)
} }
},
replaceAllInFile: async (result: SearchResult) => {
await plugin.call('editor', 'discardHighlight')
const content = await plugin.call(
'fileManager',
'readFile',
result.path
)
const replaced = replaceAllInFile(content, createRegExFromFind(), state.replace)
await plugin.call(
'fileManager',
'setFile',
result.path,
replaced
)
await plugin.call(
'fileManager',
'open',
result.path
)
setUndoState(content, replaced, result.path)
},
undoReplace: async (buffer: undoBufferRecord) => {
const content = await plugin.call(
'fileManager',
'readFile',
buffer.path
)
if (buffer.newContent !== content) {
value.clearUndo()
throw new Error('Can not undo replace, file has been changed.')
}
await plugin.call(
'fileManager',
'setFile',
buffer.path,
buffer.oldContent
)
await plugin.call(
'fileManager',
'open',
buffer.path
)
value.clearUndo()
},
clearUndo: () => {
dispatch ({
type: 'CLEAR_UNDO',
payload: undefined
})
} }
} }
const reloadStateForFile = async (file: string) => { const reloadStateForFile = async (file: string) => {
await value.reloadFile(file) await value.reloadFile(file)
} }
useEffect(() => { useEffect(() => {
plugin.on('filePanel', 'setWorkspace', () => { plugin.on('filePanel', 'setWorkspace', () => {
value.setSearchResults(null) value.setSearchResults(null)
value.clearUndo()
}) })
plugin.on('fileManager', 'fileSaved', async file => { plugin.on('fileManager', 'fileSaved', async file => {
await reloadStateForFile(file) await reloadStateForFile(file)
@ -215,22 +269,46 @@ export const SearchProvider = ({
const results = [] const results = []
paths.split(',').forEach(path => { paths.split(',').forEach(path => {
path = path.trim() path = path.trim()
if(path.startsWith('*.')) path = path.replace(/(\*\.)/g, '**/*.') if (path.startsWith('*.')) path = path.replace(/(\*\.)/g, '**/*.')
if(path.endsWith('/*') && !path.endsWith('/**/*')) path = path.replace(/(\*)/g, '**/*.*') if (path.endsWith('/*') && !path.endsWith('/**/*')) path = path.replace(/(\*)/g, '**/*.*')
results.push(path) results.push(path)
}) })
return results return results
} }
const setUndoState = async (oldContent: string, newContent: string, path: string) => {
const workspace = await plugin.call('filePanel', 'getCurrentWorkspace')
const undo = {
oldContent,
newContent,
path,
workspace
}
dispatch({
type: 'SET_UNDO',
payload: undo
})
}
const createRegExFromFind = () => {
let flags = 'g'
let find = state.find
if (!state.casesensitive) flags += 'i'
if (!state.useRegExp) find = escapeRegExp(find)
if (state.matchWord) find = `\\b${find}\\b`
const re = new RegExp(find, flags)
return re
}
useEffect(() => { useEffect(() => {
if (state.find) { if (state.find) {
(async () => { (async () => {
const files = await getDirectory('/', plugin) const files = await getDirectory('/', plugin)
const pathFilter: any = {} const pathFilter: any = {}
if (state.include){ if (state.include) {
pathFilter.include = setGlobalExpression(state.include) pathFilter.include = setGlobalExpression(state.include)
} }
if (state.exclude){ if (state.exclude) {
pathFilter.exclude = setGlobalExpression(state.exclude) pathFilter.exclude = setGlobalExpression(state.exclude)
} }
const filteredFiles = files.filter(filePathFilter(pathFilter)).map(file => { const filteredFiles = files.filter(filePathFilter(pathFilter)).map(file => {

@ -1,4 +1,4 @@
import { Action, SearchingInitialState, SearchState } from "../types" import { Action, SearchingInitialState, SearchState, undoBufferRecord } from "../types"
export const SearchReducer = (state: SearchState = SearchingInitialState, action: Action) => { export const SearchReducer = (state: SearchState = SearchingInitialState, action: Action) => {
switch (action.type) { switch (action.type) {
@ -41,21 +41,50 @@ export const SearchReducer = (state: SearchState = SearchingInitialState, action
searchResults: action.payload, searchResults: action.payload,
count: 0 count: 0
} }
case 'SET_UNDO': {
const undoState = {
newContent : action.payload.newContent,
oldContent: action.payload.oldContent,
path: action.payload.path,
workspace: action.payload.workspace,
timeStamp: Date.now()
}
state.undoBuffer = [undoState]
return {
...state,
}
}
case 'CLEAR_UNDO': {
state.undoBuffer = []
return {
...state,
}
}
case 'UPDATE_COUNT': case 'UPDATE_COUNT':
if (state.searchResults) { if (state.searchResults) {
const findFile = state.searchResults.find(file => file.filename === action.payload.file) const findFile = state.searchResults.find(file => file.filename === action.payload.file)
let count = 0 let count = 0
let fileCount = 0
let clipped = false
if (findFile) { if (findFile) {
findFile.count = action.payload.count findFile.count = action.payload.count
} }
state.searchResults.forEach(file => { state.searchResults.forEach(file => {
if (file.count) { if (file.count) {
count += file.count if(file.count > state.maxLines) {
clipped = true
count += state.maxLines
}else{
count += file.count
}
fileCount++
} }
}) })
return { return {
...state, ...state,
count: count count: count,
fileCount,
clipped
} }
} else { } else {
return state return state

@ -35,6 +35,14 @@ export interface SearchResult {
count: number count: number
} }
export interface undoBufferRecord {
workspace: string,
path: string,
newContent: string,
timeStamp: number,
oldContent: string
}
export interface SearchState { export interface SearchState {
find: string, find: string,
searchResults: SearchResult[], searchResults: SearchResult[],
@ -48,9 +56,11 @@ export interface SearchState {
useRegExp: boolean, useRegExp: boolean,
timeStamp: number, timeStamp: number,
count: number, count: number,
maxResults: number fileCount: number,
maxFiles: number, maxFiles: number,
maxLines: number maxLines: number
clipped: boolean,
undoBuffer: undoBufferRecord[],
} }
export const SearchingInitialState: SearchState = { export const SearchingInitialState: SearchState = {
@ -66,7 +76,9 @@ export const SearchingInitialState: SearchState = {
replaceWithOutConfirmation: false, replaceWithOutConfirmation: false,
timeStamp: 0, timeStamp: 0,
count: 0, count: 0,
maxResults: 1500, fileCount: 0,
maxFiles: 100, maxFiles: 5000,
maxLines: 200 maxLines: 5000,
clipped: false,
undoBuffer: null
} }
Loading…
Cancel
Save