From cd700231a94870b2845c91e656f67e9cb165c0e0 Mon Sep 17 00:00:00 2001 From: bunsenstraat Date: Tue, 15 Mar 2022 12:41:08 +0100 Subject: [PATCH] merge --- .../search/src/lib/components/Exclude.tsx | 2 +- .../search/src/lib/components/Include.tsx | 2 +- .../search/src/lib/components/Replace.tsx | 2 +- .../search/src/lib/components/Search.tsx | 2 + .../search/src/lib/components/Undo.tsx | 29 +++++ .../src/lib/components/results/ResultItem.tsx | 26 +++- .../src/lib/components/results/Results.tsx | 4 +- .../lib/components/results/SearchHelper.ts | 3 + .../search/src/lib/context/context.tsx | 114 +++++++++++++++--- .../search/src/lib/reducers/Reducer.ts | 37 +++++- libs/remix-ui/search/src/lib/types/index.ts | 20 ++- 11 files changed, 207 insertions(+), 34 deletions(-) create mode 100644 libs/remix-ui/search/src/lib/components/Undo.tsx diff --git a/libs/remix-ui/search/src/lib/components/Exclude.tsx b/libs/remix-ui/search/src/lib/components/Exclude.tsx index c334116a6c..9047e26ce7 100644 --- a/libs/remix-ui/search/src/lib/components/Exclude.tsx +++ b/libs/remix-ui/search/src/lib/components/Exclude.tsx @@ -18,7 +18,7 @@ export const Exclude = props => { return ( <>
- + { return ( <>
- + { return ( <>
- + { @@ -21,6 +22,7 @@ return ( +
diff --git a/libs/remix-ui/search/src/lib/components/Undo.tsx b/libs/remix-ui/search/src/lib/components/Undo.tsx new file mode 100644 index 0000000000..e44cd23c88 --- /dev/null +++ b/libs/remix-ui/search/src/lib/components/Undo.tsx @@ -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 ? + : null} + ) +} \ No newline at end of file diff --git a/libs/remix-ui/search/src/lib/components/results/ResultItem.tsx b/libs/remix-ui/search/src/lib/components/results/ResultItem.tsx index 0728d220a1..c0d1ee1ff6 100644 --- a/libs/remix-ui/search/src/lib/components/results/ResultItem.tsx +++ b/libs/remix-ui/search/src/lib/components/results/ResultItem.tsx @@ -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(false) @@ -17,7 +18,7 @@ export const ResultItem = (props: ResultItemProps) => { const [toggleExpander, setToggleExpander] = useState(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 ?
Loading...
: null} {!toggleExpander && !loading ? (
- {lines.map((line, index) => ( + {state.replaceEnabled?
replace()} className='btn btn-primary btn-block mb-2 btn-sm'>Replace all
:null} + {lines.map((line, index) => ( index < state.maxLines ? { const { state } = useContext(SearchContext) return (
- {state.find ?
{state.count} results
: null} - {state.find && state.count >= state.maxResults?
The result set only contains a subset of all matches

Please narrow down your search.
: null} + {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 index : 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 267fa2a92e..9d131ba620 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' @@ -29,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 @@ -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 + undoReplace: (buffer: undoBufferRecord) => Promise + clearUndo: () => void } export const SearchContext = createContext(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 => { diff --git a/libs/remix-ui/search/src/lib/reducers/Reducer.ts b/libs/remix-ui/search/src/lib/reducers/Reducer.ts index 47b9cef21f..b40d8ab4c6 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) { @@ -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 diff --git a/libs/remix-ui/search/src/lib/types/index.ts b/libs/remix-ui/search/src/lib/types/index.ts index 05f2200b1d..aa11435099 100644 --- a/libs/remix-ui/search/src/lib/types/index.ts +++ b/libs/remix-ui/search/src/lib/types/index.ts @@ -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 } \ No newline at end of file