+ ) : null}
>
)
}
diff --git a/libs/remix-ui/search/src/lib/components/Replace.tsx b/libs/remix-ui/search/src/lib/components/Replace.tsx
index 7c39191e6d..a8bd50177f 100644
--- a/libs/remix-ui/search/src/lib/components/Replace.tsx
+++ b/libs/remix-ui/search/src/lib/components/Replace.tsx
@@ -11,8 +11,8 @@ export const Replace = props => {
return (
<>
-
))}
>
diff --git a/libs/remix-ui/search/src/lib/components/results/Results.tsx b/libs/remix-ui/search/src/lib/components/results/Results.tsx
index b2094d23c9..3e5472915a 100644
--- a/libs/remix-ui/search/src/lib/components/results/Results.tsx
+++ b/libs/remix-ui/search/src/lib/components/results/Results.tsx
@@ -5,13 +5,13 @@ 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.find ?
showing {state.count} results {state.fileCount} in files
: null}
+ {state.find && state.clipped?
The result set only shows a subset of all matches
Please narrow down your search.
: null}
+ {state.searchResults &&
state.searchResults.map((result, index) => {
- return
+ return index
: null
})}
- {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
index 88ae6b1de1..fb6b096df2 100644
--- a/libs/remix-ui/search/src/lib/components/results/SearchHelper.ts
+++ b/libs/remix-ui/search/src/lib/components/results/SearchHelper.ts
@@ -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
diff --git a/libs/remix-ui/search/src/lib/context/context.tsx b/libs/remix-ui/search/src/lib/context/context.tsx
index bbc3c72795..6d270ba1b2 100644
--- a/libs/remix-ui/search/src/lib/context/context.tsx
+++ b/libs/remix-ui/search/src/lib/context/context.tsx
@@ -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'
@@ -20,6 +22,7 @@ export interface SearchingStateInterface {
state: SearchState
setFind: (value: string) => void
setReplace: (value: string) => void
+ setReplaceEnabled: (value: boolean) => void
setInclude: (value: string) => void
setExclude: (value: string) => void
setCaseSensitive: (value: boolean) => void
@@ -28,7 +31,7 @@ export interface SearchingStateInterface {
setSearchResults: (value: SearchResult[]) => void
findText: (path: string) => Promise
hightLightInPath: (result: SearchResult, line: SearchResultLineLine) => void
- replaceText: (result: SearchResult, line: SearchResultLineLine) => void
+ replaceText: (result: SearchResult, line: SearchResultLineLine) => Promise
reloadFile: (file: string) => void
toggleCaseSensitive: () => void
toggleMatchWholeWord: () => void
@@ -36,6 +39,9 @@ export interface SearchingStateInterface {
setReplaceWithoutConfirmation: (value: boolean) => void
disableForceReload: (file: string) => void
updateCount: (count: number, file: string) => void
+ replaceAllInFile: (result: SearchResult) => Promise
+ undoReplace: (buffer: undoBufferRecord) => Promise
+ clearUndo: () => void
}
export const SearchContext = createContext(null)
@@ -63,6 +69,12 @@ export const SearchProvider = ({
payload: value
})
},
+ setReplaceEnabled: (value: boolean) => {
+ dispatch({
+ type: 'SET_REPLACE_ENABLED',
+ payload: value
+ })
+ },
setInclude: (value: string) => {
dispatch({
type: 'SET_INCLUDE',
@@ -135,26 +147,32 @@ export const SearchProvider = ({
payload: file
})
},
+ setCurrentFile: (file: string) => {
+ dispatch({
+ type: 'SET_CURRENT_FILE',
+ payload: file
+ })
+ },
+ setCurrentWorkspace: (workspace: any) => {
+ dispatch({
+ type: 'SET_CURRENT_WORKSPACE',
+ payload: workspace
+ })
+ },
updateCount: (count: number, file: string) => {
dispatch({
type: 'UPDATE_COUNT',
- payload: {count, file}
+ payload: { count, file }
})
},
findText: async (path: string) => {
if (!plugin) return
try {
- if (state.find.length < 3) return
+ 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,
@@ -162,6 +180,7 @@ export const SearchProvider = ({
) => {
await plugin.call('editor', 'discardHighlight')
await plugin.call('editor', 'highlight', line.position, result.path)
+ await plugin.call('editor', 'revealRange', line.position.start.line, line.position.start.column, line.position.end.line, line.position.end.column)
},
replaceText: async (result: SearchResult, line: SearchResultLineLine) => {
try {
@@ -172,30 +191,99 @@ 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)
+ },
+ setUndoEnabled: (path:string, workspace: string, content: string) => {
+ dispatch({
+ type: 'SET_UNDO_ENABLED',
+ payload: {
+ path,
+ workspace,
+ content
+ }
+ })
+ },
+ undoReplace: async (buffer: undoBufferRecord) => {
+ const content = await plugin.call(
+ 'fileManager',
+ 'readFile',
+ buffer.path
+ )
+ if (buffer.newContent !== content) {
+ 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
+ )
+ },
+ 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', () => {
+ plugin.on('filePanel', 'setWorkspace', async (workspace) => {
value.setSearchResults(null)
+ value.clearUndo()
+ value.setCurrentWorkspace(workspace.name)
})
plugin.on('fileManager', 'fileSaved', async file => {
await reloadStateForFile(file)
+ await checkUndoState(file)
})
+ plugin.on('fileManager', 'currentFileChanged', async file => {
+ value.setCurrentFile(file)
+ await checkUndoState(file)
+ })
+
return () => {
plugin.off('fileManager', 'fileChanged')
plugin.off('filePanel', 'setWorkspace')
@@ -207,22 +295,61 @@ 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 checkUndoState = async (path: string) => {
+ if (!plugin) return
+ try {
+ const content = await plugin.call(
+ 'fileManager',
+ 'readFile',
+ path
+ )
+ const workspace = await plugin.call('filePanel', 'getCurrentWorkspace')
+ value.setUndoEnabled(path, workspace.name, content)
+ } catch (e) {
+ console.log(e)
+ }
+ }
+
+ const setUndoState = async (oldContent: string, newContent: string, path: string) => {
+ const workspace = await plugin.call('filePanel', 'getCurrentWorkspace')
+ const undo = {
+ oldContent,
+ newContent,
+ path,
+ workspace: workspace.name
+ }
+ 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 => {
diff --git a/libs/remix-ui/search/src/lib/reducers/Reducer.ts b/libs/remix-ui/search/src/lib/reducers/Reducer.ts
index 05e9efaecb..3e188f61fb 100644
--- a/libs/remix-ui/search/src/lib/reducers/Reducer.ts
+++ b/libs/remix-ui/search/src/lib/reducers/Reducer.ts
@@ -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) {
@@ -15,6 +15,12 @@ export const SearchReducer = (state: SearchState = SearchingInitialState, action
replace: action.payload,
}
+ case 'SET_REPLACE_ENABLED':
+ return {
+ ...state,
+ replaceEnabled: action.payload,
+ }
+
case 'SET_INCLUDE':
return {
...state,
@@ -35,21 +41,60 @@ export const SearchReducer = (state: SearchState = SearchingInitialState, action
searchResults: action.payload,
count: 0
}
+ case 'SET_UNDO_ENABLED':
+ if(state.undoBuffer[`${action.payload.workspace}/${action.payload.path}`]){
+ state.undoBuffer[`${action.payload.workspace}/${action.payload.path}`].enabled = (action.payload.content === state.undoBuffer[`${action.payload.workspace}/${action.payload.path}`].newContent)
+ state.undoBuffer[`${action.payload.workspace}/${action.payload.path}`].visible = (action.payload.content !== state.undoBuffer[`${action.payload.workspace}/${action.payload.path}`].oldContent)
+ }
+ return {
+ ...state,
+ }
+ case 'SET_UNDO': {
+ const undoState = {
+ newContent : action.payload.newContent,
+ oldContent: action.payload.oldContent,
+ path: action.payload.path,
+ workspace: action.payload.workspace,
+ timeStamp: Date.now(),
+ enabled: true,
+ visible: true
+ }
+ state.undoBuffer[`${undoState.workspace}/${undoState.path}`] = 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
@@ -85,6 +130,16 @@ export const SearchReducer = (state: SearchState = SearchingInitialState, action
return {
...state,
}
+ case 'SET_CURRENT_FILE':
+ return {
+ ...state,
+ currentFile: action.payload,
+ }
+ case 'SET_CURRENT_WORKSPACE':
+ return {
+ ...state,
+ workspace: action.payload,
+ }
case 'RELOAD_FILE':
if (state.searchResults) {
const findFile = state.searchResults.find(file => file.filename === action.payload)
diff --git a/libs/remix-ui/search/src/lib/search.css b/libs/remix-ui/search/src/lib/search.css
index 895ce83e7f..acc789d51c 100644
--- a/libs/remix-ui/search/src/lib/search.css
+++ b/libs/remix-ui/search/src/lib/search.css
@@ -21,7 +21,7 @@
.search_plugin_find-part {
display: flex;
flex-direction: column;
- padding: 2px;
+ padding-top: 5px;
}
.search_plugin_controls {
@@ -64,6 +64,7 @@
.search_plugin_search_tab mark {
padding: 0;
+ white-space: pre;
}
.search_plugin_search_tab .search_plugin_search_line_container .search_plugin_search_control {
@@ -103,4 +104,33 @@
.search_plugin_search_tab .search_plugin_result_count_number {
font-size: x-small;
+}
+
+.search_plugin_find_container {
+ display: flex;
+ flex-direction: row;
+}
+
+.search_plugin_find_container_internal {
+ display: flex;
+ flex-direction: column;
+ flex-grow: 1;
+}
+
+.search_plugin_find_container_arrow {
+ display: flex !important;
+ align-items: center;
+ cursor: pointer !important;
+}
+
+.search_plugin_wrap_summary_replace {
+ display: flex;
+ flex-direction: row;
+ justify-content: flex-end;
+}
+
+.undo-button {
+ white-space: pre;
+ text-overflow: ellipsis;
+ overflow: hidden;
}
\ 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
index af76487c51..231c4ebf93 100644
--- a/libs/remix-ui/search/src/lib/types/index.ts
+++ b/libs/remix-ui/search/src/lib/types/index.ts
@@ -35,10 +35,20 @@ export interface SearchResult {
count: number
}
+export interface undoBufferRecord {
+ workspace: string,
+ path: string,
+ newContent: string,
+ timeStamp: number,
+ oldContent: string,
+ enabled: boolean,
+ visible: boolean
+}
export interface SearchState {
find: string,
searchResults: SearchResult[],
replace: string,
+ replaceEnabled: boolean,
include: string,
exclude: string,
casesensitive: boolean,
@@ -47,7 +57,13 @@ export interface SearchState {
useRegExp: boolean,
timeStamp: number,
count: number,
- maxResults: number
+ fileCount: number,
+ maxFiles: number,
+ maxLines: number
+ clipped: boolean,
+ undoBuffer: Record[],
+ currentFile: string,
+ workspace: string
}
export const SearchingInitialState: SearchState = {
@@ -55,6 +71,7 @@ export const SearchingInitialState: SearchState = {
replace: '',
include: '',
exclude: '',
+ replaceEnabled: false,
searchResults: [],
casesensitive: false,
matchWord: false,
@@ -62,5 +79,11 @@ export const SearchingInitialState: SearchState = {
replaceWithOutConfirmation: false,
timeStamp: 0,
count: 0,
- maxResults: 500
+ fileCount: 0,
+ maxFiles: 5000,
+ maxLines: 5000,
+ clipped: false,
+ undoBuffer: null,
+ currentFile: '',
+ workspace: ''
}
\ No newline at end of file