pull/2170/head
bunsenstraat 3 years ago
parent db3f124b5a
commit fb9f9e2c85
  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 (
<>
<div className="search_plugin_find-part pl-3">
<label>files to exclude</label>
<label className='mt-2'>files to exclude</label>
<input
id='search_exclude'
placeholder="Exclude ie .git/**/*"

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

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

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

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

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

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

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

@ -1,4 +1,4 @@
import { Action, SearchingInitialState, SearchState } from "../types"
import { Action, SearchingInitialState, SearchState, undoBufferRecord } from "../types"
export const SearchReducer = (state: SearchState = SearchingInitialState, action: Action) => {
switch (action.type) {
@ -41,21 +41,50 @@ export const SearchReducer = (state: SearchState = SearchingInitialState, action
searchResults: action.payload,
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':
if (state.searchResults) {
const findFile = state.searchResults.find(file => file.filename === action.payload.file)
let count = 0
let fileCount = 0
let clipped = false
if (findFile) {
findFile.count = action.payload.count
}
state.searchResults.forEach(file => {
if (file.count) {
count += file.count
if (file.count) {
if(file.count > state.maxLines) {
clipped = true
count += state.maxLines
}else{
count += file.count
}
fileCount++
}
})
return {
...state,
count: count
count: count,
fileCount,
clipped
}
} else {
return state

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