Skip to content

Commit ab795bb

Browse files
committed
Impl refactoring view
1 parent 8c9bb23 commit ab795bb

File tree

4 files changed

+558
-1
lines changed

4 files changed

+558
-1
lines changed

helix-term/src/commands.rs

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,7 @@ impl MappableCommand {
248248
extend_search_prev, "Add previous search match to selection",
249249
search_selection, "Use current selection as search pattern",
250250
global_search, "Global search in workspace folder",
251+
global_refactor, "Global refactoring in workspace folder",
251252
extend_line, "Select current line, if already selected, extend to another line based on the anchor",
252253
extend_line_below, "Select current line, if already selected, extend to next line",
253254
extend_line_above, "Select current line, if already selected, extend to previous line",
@@ -1967,6 +1968,188 @@ fn global_search(cx: &mut Context) {
19671968
};
19681969
cx.jobs.callback(show_picker);
19691970
}
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+
}
19702153

19712154
enum Extend {
19722155
Above,

helix-term/src/ui/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ pub mod overlay;
99
mod picker;
1010
pub mod popup;
1111
mod prompt;
12+
mod refactor;
1213
mod spinner;
1314
mod statusline;
1415
mod text;
@@ -22,6 +23,7 @@ pub use menu::Menu;
2223
pub use picker::{FileLocation, FilePicker, Picker};
2324
pub use popup::Popup;
2425
pub use prompt::{Prompt, PromptEvent};
26+
pub use refactor::RefactorView;
2527
pub use spinner::{ProgressSpinners, Spinner};
2628
pub use text::Text;
2729

0 commit comments

Comments
 (0)