@@ -248,6 +248,7 @@ impl MappableCommand {
248
248
extend_search_prev, "Add previous search match to selection" ,
249
249
search_selection, "Use current selection as search pattern" ,
250
250
global_search, "Global search in workspace folder" ,
251
+ global_refactor, "Global refactoring in workspace folder" ,
251
252
extend_line, "Select current line, if already selected, extend to another line based on the anchor" ,
252
253
extend_line_below, "Select current line, if already selected, extend to next line" ,
253
254
extend_line_above, "Select current line, if already selected, extend to previous line" ,
@@ -1967,6 +1968,188 @@ fn global_search(cx: &mut Context) {
1967
1968
} ;
1968
1969
cx. jobs . callback ( show_picker) ;
1969
1970
}
1971
+ fn global_refactor ( cx : & mut Context ) {
1972
+ let ( all_matches_sx, all_matches_rx) =
1973
+ tokio:: sync:: mpsc:: unbounded_channel :: < ( PathBuf , usize , String ) > ( ) ;
1974
+ let config = cx. editor . config ( ) ;
1975
+ let smart_case = config. search . smart_case ;
1976
+ let file_picker_config = config. file_picker . clone ( ) ;
1977
+
1978
+ let reg = cx. register . unwrap_or ( '/' ) ;
1979
+
1980
+ // Restrict to current file type if possible
1981
+ let file_extension = doc ! ( cx. editor) . path ( ) . and_then ( |f| f. extension ( ) ) ;
1982
+ let file_glob = if let Some ( file_glob) = file_extension. and_then ( |f| f. to_str ( ) ) {
1983
+ let mut tb = ignore:: types:: TypesBuilder :: new ( ) ;
1984
+ tb. add ( "p" , & ( String :: from ( "*." ) + file_glob) )
1985
+ . ok ( )
1986
+ . and_then ( |_| {
1987
+ tb. select ( "all" ) ;
1988
+ tb. build ( ) . ok ( )
1989
+ } )
1990
+ } else {
1991
+ None
1992
+ } ;
1993
+
1994
+ let completions = search_completions ( cx, Some ( reg) ) ;
1995
+ ui:: regex_prompt (
1996
+ cx,
1997
+ "global-refactor:" . into ( ) ,
1998
+ Some ( reg) ,
1999
+ move |_editor : & Editor , input : & str | {
2000
+ completions
2001
+ . iter ( )
2002
+ . filter ( |comp| comp. starts_with ( input) )
2003
+ . map ( |comp| ( 0 .., std:: borrow:: Cow :: Owned ( comp. clone ( ) ) ) )
2004
+ . collect ( )
2005
+ } ,
2006
+ move |editor, regex, event| {
2007
+ if event != PromptEvent :: Validate {
2008
+ return ;
2009
+ }
2010
+
2011
+ if let Ok ( matcher) = RegexMatcherBuilder :: new ( )
2012
+ . case_smart ( smart_case)
2013
+ . build ( regex. as_str ( ) )
2014
+ {
2015
+ let searcher = SearcherBuilder :: new ( )
2016
+ . binary_detection ( BinaryDetection :: quit ( b'\x00' ) )
2017
+ . build ( ) ;
2018
+
2019
+ let mut checked = HashSet :: < PathBuf > :: new ( ) ;
2020
+ let file_extension = editor. documents [ & editor. tree . get ( editor. tree . focus ) . doc ]
2021
+ . path ( )
2022
+ . and_then ( |f| f. extension ( ) ) ;
2023
+ for doc in editor. documents ( ) {
2024
+ searcher
2025
+ . clone ( )
2026
+ . search_slice (
2027
+ matcher. clone ( ) ,
2028
+ doc. text ( ) . to_string ( ) . as_bytes ( ) ,
2029
+ sinks:: UTF8 ( |line_num, matched| {
2030
+ if let Some ( path) = doc. path ( ) {
2031
+ if let Some ( extension) = path. extension ( ) {
2032
+ if let Some ( file_extension) = file_extension {
2033
+ if file_extension == extension {
2034
+ all_matches_sx
2035
+ . send ( (
2036
+ path. clone ( ) ,
2037
+ line_num as usize - 1 ,
2038
+ String :: from (
2039
+ matched
2040
+ . strip_suffix ( "\r \n " )
2041
+ . or ( matched. strip_suffix ( "\n " ) )
2042
+ . unwrap_or ( matched) ,
2043
+ ) ,
2044
+ ) )
2045
+ . unwrap ( ) ;
2046
+ }
2047
+ }
2048
+ }
2049
+ // Exclude from file search
2050
+ checked. insert ( path. clone ( ) ) ;
2051
+ }
2052
+ Ok ( true )
2053
+ } ) ,
2054
+ )
2055
+ . ok ( ) ;
2056
+ }
2057
+
2058
+ let search_root = std:: env:: current_dir ( )
2059
+ . expect ( "Global search error: Failed to get current dir" ) ;
2060
+ let mut wb = WalkBuilder :: new ( search_root) ;
2061
+ wb. hidden ( file_picker_config. hidden )
2062
+ . parents ( file_picker_config. parents )
2063
+ . ignore ( file_picker_config. ignore )
2064
+ . git_ignore ( file_picker_config. git_ignore )
2065
+ . git_global ( file_picker_config. git_global )
2066
+ . git_exclude ( file_picker_config. git_exclude )
2067
+ . max_depth ( file_picker_config. max_depth ) ;
2068
+ if let Some ( file_glob) = & file_glob {
2069
+ wb. types ( file_glob. clone ( ) ) ;
2070
+ }
2071
+ wb. build_parallel ( ) . run ( || {
2072
+ let mut searcher = searcher. clone ( ) ;
2073
+ let matcher = matcher. clone ( ) ;
2074
+ let all_matches_sx = all_matches_sx. clone ( ) ;
2075
+ let checked = checked. clone ( ) ;
2076
+ Box :: new ( move |entry : Result < DirEntry , ignore:: Error > | -> WalkState {
2077
+ let entry = match entry {
2078
+ Ok ( entry) => entry,
2079
+ Err ( _) => return WalkState :: Continue ,
2080
+ } ;
2081
+
2082
+ match entry. file_type ( ) {
2083
+ Some ( entry) if entry. is_file ( ) => { }
2084
+ // skip everything else
2085
+ _ => return WalkState :: Continue ,
2086
+ } ;
2087
+
2088
+ let result = searcher. search_path (
2089
+ & matcher,
2090
+ entry. path ( ) ,
2091
+ sinks:: UTF8 ( |line_num, matched| {
2092
+ let path = entry. clone ( ) . into_path ( ) ;
2093
+ if !checked. contains ( & path) {
2094
+ all_matches_sx
2095
+ . send ( (
2096
+ path,
2097
+ line_num as usize - 1 ,
2098
+ String :: from (
2099
+ matched
2100
+ . strip_suffix ( "\r \n " )
2101
+ . or ( matched. strip_suffix ( "\n " ) )
2102
+ . unwrap_or ( matched) ,
2103
+ ) ,
2104
+ ) )
2105
+ . unwrap ( ) ;
2106
+ }
2107
+ Ok ( true )
2108
+ } ) ,
2109
+ ) ;
2110
+
2111
+ if let Err ( err) = result {
2112
+ log:: error!( "Global search error: {}, {}" , entry. path( ) . display( ) , err) ;
2113
+ }
2114
+ WalkState :: Continue
2115
+ } )
2116
+ } ) ;
2117
+ }
2118
+ } ,
2119
+ ) ;
2120
+
2121
+ let show_refactor = async move {
2122
+ let all_matches: Vec < ( PathBuf , usize , String ) > =
2123
+ UnboundedReceiverStream :: new ( all_matches_rx) . collect ( ) . await ;
2124
+ let call: job:: Callback =
2125
+ Box :: new ( move |editor : & mut Editor , compositor : & mut Compositor | {
2126
+ if all_matches. is_empty ( ) {
2127
+ editor. set_status ( "No matches found" ) ;
2128
+ return ;
2129
+ }
2130
+ let mut document_data: HashMap < PathBuf , Vec < ( usize , String ) > > = HashMap :: new ( ) ;
2131
+ for ( path, line, text) in all_matches {
2132
+ if let Some ( vec) = document_data. get_mut ( & path) {
2133
+ vec. push ( ( line, text) ) ;
2134
+ } else {
2135
+ let v = Vec :: from ( [ ( line, text) ] ) ;
2136
+ document_data. insert ( path, v) ;
2137
+ }
2138
+ }
2139
+
2140
+ let editor_view = compositor. find :: < ui:: EditorView > ( ) . unwrap ( ) ;
2141
+ let language_id = doc ! ( editor)
2142
+ . language_id ( )
2143
+ . and_then ( |language_id| Some ( String :: from ( language_id) ) ) ;
2144
+
2145
+ let re_view =
2146
+ ui:: RefactorView :: new ( document_data, editor, editor_view, language_id) ;
2147
+ compositor. push ( Box :: new ( re_view) ) ;
2148
+ } ) ;
2149
+ Ok ( call)
2150
+ } ;
2151
+ cx. jobs . callback ( show_refactor) ;
2152
+ }
1970
2153
1971
2154
enum Extend {
1972
2155
Above ,
0 commit comments