diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 26895576c284b..03693e2bfd249 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -30,7 +30,7 @@ use helix_core::{ use helix_view::{ apply_transaction, clipboard::ClipboardType, - document::{FormatterError, Mode, SCRATCH_BUFFER_NAME}, + document::{FormatterError, Mode, REFACTOR_BUFFER_NAME, SCRATCH_BUFFER_NAME}, editor::{Action, Motion}, info::Info, input::KeyEvent, @@ -2039,6 +2039,8 @@ fn global_refactor(cx: &mut Context) { None }; + let encoding = Some(doc!(cx.editor).encoding()); + let completions = search_completions(cx, Some(reg)); ui::regex_prompt( cx, @@ -2086,7 +2088,9 @@ fn global_refactor(cx: &mut Context) { String::from( matched .strip_suffix("\r\n") - .or(matched.strip_suffix("\n")) + .or_else(|| { + matched.strip_suffix('\n') + }) .unwrap_or(matched), ), )) @@ -2146,7 +2150,7 @@ fn global_refactor(cx: &mut Context) { String::from( matched .strip_suffix("\r\n") - .or(matched.strip_suffix("\n")) + .or_else(|| matched.strip_suffix('\n')) .unwrap_or(matched), ), )) @@ -2169,32 +2173,43 @@ fn global_refactor(cx: &mut Context) { let show_refactor = async move { let all_matches: Vec<(PathBuf, usize, String)> = UnboundedReceiverStream::new(all_matches_rx).collect().await; - let call: job::Callback = Callback::EditorCompositor(Box::new( - move |editor: &mut Editor, compositor: &mut Compositor| { - if all_matches.is_empty() { - editor.set_status("No matches found"); - return; - } - let mut document_data: HashMap> = HashMap::new(); - for (path, line, text) in all_matches { - if let Some(vec) = document_data.get_mut(&path) { - vec.push((line, text)); - } else { - let v = Vec::from([(line, text)]); - document_data.insert(path, v); - } + let call: job::Callback = Callback::Editor(Box::new(move |editor: &mut Editor| { + if all_matches.is_empty() { + editor.set_status("No matches found"); + return; + } + let mut matches: HashMap> = HashMap::new(); + for (path, line, text) in all_matches { + if let Some(vec) = matches.get_mut(&path) { + vec.push((line, text)); + } else { + let v = Vec::from([(line, text)]); + matches.insert(path, v); } + } - let editor_view = compositor.find::().unwrap(); - let language_id = doc!(editor) - .language_id() - .and_then(|language_id| Some(String::from(language_id))); + let language_id = doc!(editor).language_id().map(String::from); - let re_view = - ui::RefactorView::new(document_data, editor, editor_view, language_id); - compositor.push(Box::new(re_view)); - }, - )); + let mut doc_text = Rope::new(); + let mut line_map = HashMap::new(); + + let mut count = 0; + for (key, value) in &matches { + for (line, text) in value { + doc_text.insert(doc_text.len_chars(), text); + doc_text.insert(doc_text.len_chars(), "\n"); + line_map.insert((key.clone(), *line), count); + count += 1; + } + } + doc_text.split_off(doc_text.len_chars().saturating_sub(1)); + let mut doc = Document::refactor(doc_text, matches, line_map, encoding); + if let Some(language_id) = language_id { + doc.set_language_by_language_id(&language_id, editor.syn_loader.clone()) + .ok(); + }; + editor.new_file_from_document(Action::Replace, doc); + })); Ok(call) }; cx.jobs.callback(show_refactor); @@ -2488,6 +2503,7 @@ fn buffer_picker(cx: &mut Context) { path: Option, is_modified: bool, is_current: bool, + is_refactor: bool, } impl ui::menu::Item for BufferMeta { @@ -2498,9 +2514,13 @@ fn buffer_picker(cx: &mut Context) { .path .as_deref() .map(helix_core::path::get_relative_path); - let path = match path.as_deref().and_then(Path::to_str) { - Some(path) => path, - None => SCRATCH_BUFFER_NAME, + let path = if self.is_refactor { + REFACTOR_BUFFER_NAME + } else { + match path.as_deref().and_then(Path::to_str) { + Some(path) => path, + None => SCRATCH_BUFFER_NAME, + } }; let mut flags = Vec::new(); @@ -2525,6 +2545,13 @@ fn buffer_picker(cx: &mut Context) { path: doc.path().cloned(), is_modified: doc.is_modified(), is_current: doc.id() == current, + is_refactor: matches!( + &doc.document_type, + helix_view::document::DocumentType::Refactor { + matches: _, + line_map: _ + } + ), }; let picker = FilePicker::new( diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index cb387fcb5b641..ef824fe7bf5eb 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -1808,6 +1808,75 @@ fn run_shell_command( Ok(()) } +fn apply_refactor( + cx: &mut compositor::Context, + _args: &[Cow], + event: PromptEvent, +) -> anyhow::Result<()> { + if event != PromptEvent::Validate { + return Ok(()); + } + + let document_type = doc!(cx.editor).document_type.clone(); + + match &document_type { + helix_view::document::DocumentType::File => {} + helix_view::document::DocumentType::Refactor { matches, line_map } => { + let refactor_id = doc!(cx.editor).id(); + let replace_text = doc!(cx.editor).text().clone(); + let view = view!(cx.editor).clone(); + let mut documents: usize = 0; + let mut count: usize = 0; + for (key, value) in matches { + let mut changes = Vec::<(usize, usize, String)>::new(); + for (line, text) in value { + if let Some(re_line) = line_map.get(&(key.clone(), *line)) { + let mut replace = replace_text + .get_line(*re_line) + .unwrap_or_else(|| "\n".into()) + .to_string() + .clone(); + replace = replace.strip_suffix('\n').unwrap_or(&replace).to_string(); + if text != &replace { + changes.push((*line, text.chars().count(), replace)); + } + } + } + if !changes.is_empty() { + if let Some(doc) = cx + .editor + .open(key, Action::Load) + .ok() + .and_then(|id| cx.editor.document_mut(id)) + { + documents += 1; + let mut applychanges = Vec::<(usize, usize, Option)>::new(); + for (line, length, text) in changes { + if doc.text().len_lines() > line { + let start = doc.text().line_to_char(line); + applychanges.push(( + start, + start + length, + Some(Tendril::from(text.to_string())), + )); + count += 1; + } + } + let transaction = Transaction::change(doc.text(), applychanges.into_iter()); + apply_transaction(&transaction, doc, &view); + } + } + } + cx.editor.set_status(format!( + "Refactored {} documents, {} lines changed.", + documents, count + )); + cx.editor.close_document(refactor_id, true).ok(); + } + } + Ok(()) +} + pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[ TypableCommand { name: "quit", @@ -2323,6 +2392,13 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[ fun: run_shell_command, completer: Some(completers::directory), }, + TypableCommand { + name: "apply-refactoring", + aliases: &["ar"], + doc: "Applies refactoring", + fun: apply_refactor, + completer: None, + }, ]; pub static TYPABLE_COMMAND_MAP: Lazy> = diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index fc201853f7dc6..34cfe8e7a2862 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -32,6 +32,8 @@ use tui::buffer::Buffer as Surface; use super::lsp::SignatureHelp; use super::statusline; +const REFACTOR_NAME_WIDTH: u16 = 20; + pub struct EditorView { pub keymaps: Keymaps, on_next_key: Option>, @@ -79,7 +81,7 @@ impl EditorView { surface: &mut Surface, is_focused: bool, ) { - let inner = view.inner_area(doc); + let mut inner = view.inner_area(doc); let area = view.area; let theme = &editor.theme; let config = editor.config(); @@ -149,10 +151,21 @@ impl EditorView { Box::new(highlights) }; - Self::render_text_highlights(doc, view.offset, inner, surface, theme, highlights, &config); - Self::render_gutter(editor, doc, view, view.area, surface, theme, is_focused); - Self::render_rulers(editor, doc, view, inner, surface, theme); + match &doc.document_type { + helix_view::document::DocumentType::File => { + Self::render_gutter(editor, doc, view, view.area, surface, theme, is_focused); + Self::render_rulers(editor, doc, view, inner, surface, theme); + } + helix_view::document::DocumentType::Refactor { + matches, + line_map: _, + } => { + self.render_document_names(surface, &area, view.offset, matches); + inner = area.clip_left(REFACTOR_NAME_WIDTH).clip_bottom(1); + } + } + Self::render_text_highlights(doc, view.offset, inner, surface, theme, highlights, &config); if is_focused { Self::render_focused_view_elements(view, doc, inner, theme, surface); } @@ -210,6 +223,35 @@ impl EditorView { .for_each(|area| surface.set_style(area, ruler_theme)) } + fn render_document_names( + &self, + surface: &mut Surface, + area: &Rect, + offset: helix_core::Position, + matches: &std::collections::HashMap>, + ) { + let mut start = 0; + let mut count = 0; + for (key, value) in matches { + for (line, _) in value { + if start >= offset.row && area.y + count < area.height { + let text = key.display().to_string() + ":" + line.to_string().as_str(); + surface.set_string_truncated( + area.x as u16, + area.y + count as u16, + &text, + REFACTOR_NAME_WIDTH as usize, + |_| Style::default().fg(helix_view::theme::Color::Magenta), + true, + true, + ); + count += 1; + } + start += 1; + } + } + } + /// Get syntax highlights for a document in a view represented by the first line /// and column (`offset`) and the last line. This is done instead of using a view /// directly to enable rendering syntax highlighted docs anywhere (eg. picker preview) diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index 2e312d5718623..5b5924bff0e86 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -9,7 +9,6 @@ pub mod overlay; mod picker; pub mod popup; mod prompt; -mod refactor; mod spinner; mod statusline; mod text; @@ -23,7 +22,6 @@ pub use menu::Menu; pub use picker::{DynamicPicker, FileLocation, FilePicker, Picker}; pub use popup::Popup; pub use prompt::{Prompt, PromptEvent}; -pub use refactor::RefactorView; pub use spinner::{ProgressSpinners, Spinner}; pub use text::Text; diff --git a/helix-term/src/ui/refactor.rs b/helix-term/src/ui/refactor.rs deleted file mode 100644 index 3eb49e24f66a4..0000000000000 --- a/helix-term/src/ui/refactor.rs +++ /dev/null @@ -1,373 +0,0 @@ -use crate::{ - compositor::{Component, Compositor, Context, Event, EventResult}, - keymap::{KeyTrie, KeyTrieNode, Keymap}, -}; - -use arc_swap::access::DynGuard; -use helix_core::{ - syntax::{self, HighlightEvent}, - Rope, Tendril, Transaction, -}; -use helix_view::{ - apply_transaction, document::Mode, editor::Action, graphics::Rect, keyboard::KeyCode, - theme::Style, Document, Editor, View, -}; -use once_cell::sync::Lazy; -use std::{ - collections::{HashMap, HashSet}, - path::PathBuf, -}; - -use tui::buffer::Buffer as Surface; - -use super::EditorView; - -const UNSUPPORTED_COMMANDS: Lazy> = Lazy::new(|| { - HashSet::from([ - "global_search", - "global_refactor", - // "command_mode", - "file_picker", - "file_picker_in_current_directory", - "code_action", - "buffer_picker", - "jumplist_picker", - "symbol_picker", - "select_references_to_symbol_under_cursor", - "workspace_symbol_picker", - "diagnostics_picker", - "workspace_diagnostics_picker", - "last_picker", - "goto_definition", - "goto_type_definition", - "goto_implementation", - "goto_file", - "goto_file_hsplit", - "goto_file_vsplit", - "goto_reference", - "goto_window_top", - "goto_window_center", - "goto_window_bottom", - "goto_last_accessed_file", - "goto_last_modified_file", - "goto_last_modification", - "goto_line", - "goto_last_line", - "goto_first_diag", - "goto_last_diag", - "goto_next_diag", - "goto_prev_diag", - "goto_line_start", - "goto_line_end", - "goto_next_buffer", - "goto_previous_buffer", - "signature_help", - "completion", - "hover", - "select_next_sibling", - "select_prev_sibling", - "jump_view_right", - "jump_view_left", - "jump_view_up", - "jump_view_down", - "swap_view_right", - "swap_view_left", - "swap_view_up", - "swap_view_down", - "transpose_view", - "rotate_view", - "hsplit", - "hsplit_new", - "vsplit", - "vsplit_new", - "wonly", - "select_textobject_around", - "select_textobject_inner", - "goto_next_function", - "goto_prev_function", - "goto_next_class", - "goto_prev_class", - "goto_next_parameter", - "goto_prev_parameter", - "goto_next_comment", - "goto_prev_comment", - "goto_next_test", - "goto_prev_test", - "goto_next_paragraph", - "goto_prev_paragraph", - "dap_launch", - "dap_toggle_breakpoint", - "dap_continue", - "dap_pause", - "dap_step_in", - "dap_step_out", - "dap_next", - "dap_variables", - "dap_terminate", - "dap_edit_condition", - "dap_edit_log", - "dap_switch_thread", - "dap_switch_stack_frame", - "dap_enable_exceptions", - "dap_disable_exceptions", - "shell_pipe", - "shell_pipe_to", - "shell_insert_output", - "shell_append_output", - "shell_keep_pipe", - "suspend", - "rename_symbol", - "record_macro", - "replay_macro", - "command_palette", - ]) -}); - -pub struct RefactorView { - matches: HashMap>, - line_map: HashMap<(PathBuf, usize), usize>, - keymap: DynGuard>, - sticky: Option, - apply_prompt: bool, -} - -impl RefactorView { - pub fn new( - matches: HashMap>, - editor: &mut Editor, - editor_view: &mut EditorView, - language_id: Option, - ) -> Self { - let keymap = editor_view.keymaps.map(); - let mut review = RefactorView { - matches, - keymap, - sticky: None, - line_map: HashMap::new(), - apply_prompt: false, - }; - let mut doc_text = Rope::new(); - - let mut count = 0; - for (key, value) in &review.matches { - for (line, text) in value { - doc_text.insert(doc_text.len_chars(), &text); - doc_text.insert(doc_text.len_chars(), "\n"); - review.line_map.insert((key.clone(), *line), count); - count += 1; - } - } - doc_text.split_off(doc_text.len_chars().saturating_sub(1)); - let mut doc = Document::from(doc_text, None); - if let Some(language_id) = language_id { - doc.set_language_by_language_id(&language_id, editor.syn_loader.clone()) - .ok(); - }; - editor.new_file_from_document(Action::Replace, doc); - let doc = doc_mut!(editor); - let viewid = editor.tree.insert(View::new(doc.id(), vec![])); - editor.tree.focus = viewid; - doc.ensure_view_init(viewid); - doc.reset_selection(viewid); - - review - } - - fn apply_refactor(&self, editor: &mut Editor) -> (usize, usize) { - let replace_text = doc!(editor).text().clone(); - let mut view = view!(editor).clone(); - let mut documents: usize = 0; - let mut count: usize = 0; - for (key, value) in &self.matches { - let mut changes = Vec::<(usize, usize, String)>::new(); - for (line, text) in value { - if let Some(re_line) = self.line_map.get(&(key.clone(), *line)) { - let mut replace = replace_text - .get_line(*re_line) - .unwrap_or("\n".into()) - .to_string() - .clone(); - replace = replace.strip_suffix("\n").unwrap_or(&replace).to_string(); - if text != &replace { - changes.push((*line, text.chars().count(), replace)); - } - } - } - if !changes.is_empty() { - if let Some(doc) = editor - .open(&key, Action::Load) - .ok() - .and_then(|id| editor.document_mut(id)) - { - documents += 1; - let mut applychanges = Vec::<(usize, usize, Option)>::new(); - for (line, length, text) in changes { - if doc.text().len_lines() > line { - let start = doc.text().line_to_char(line); - applychanges.push(( - start, - start + length, - Some(Tendril::from(text.to_string())), - )); - count += 1; - } - } - let transaction = Transaction::change(doc.text(), applychanges.into_iter()); - apply_transaction(&transaction, doc, &mut view); - } - } - } - (documents, count) - } - - fn render_view(&self, editor: &Editor, surface: &mut Surface) { - let doc = doc!(editor); - let view = view!(editor); - let offset = view.offset; - let mut area = view.area; - - self.render_doc_name(surface, &mut area, offset); - let highlights = - EditorView::doc_syntax_highlights(&doc, offset, area.height, &editor.theme); - let highlights: Box> = Box::new(syntax::merge( - highlights, - EditorView::doc_selection_highlights( - editor.mode(), - &doc, - &view, - &editor.theme, - &editor.config().cursor_shape, - ), - )); - - EditorView::render_text_highlights( - &doc, - offset, - area, - surface, - &editor.theme, - highlights, - &editor.config(), - ); - } - - fn render_doc_name( - &self, - surface: &mut Surface, - area: &mut Rect, - offset: helix_core::Position, - ) { - let mut start = 0; - let mut count = 0; - for (key, value) in &self.matches { - for (line, _) in value { - if start >= offset.row && area.y + count < area.height { - let text = key.display().to_string() + ":" + line.to_string().as_str(); - surface.set_string_truncated( - area.x as u16, - area.y + count as u16, - &text, - 15, - |_| Style::default().fg(helix_view::theme::Color::Magenta), - true, - true, - ); - count += 1; - } - start += 1; - } - } - area.x = 15; - } - - #[inline] - fn close(&self, editor: &mut Editor) -> EventResult { - editor.close_document(doc!(editor).id(), true).ok(); - editor.autoinfo = None; - EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor, _cx| { - compositor.pop(); - }))) - } -} - -impl Component for RefactorView { - fn handle_event(&mut self, event: &Event, cx: &mut Context) -> EventResult { - let config = cx.editor.config(); - let (view, doc) = current!(cx.editor); - view.ensure_cursor_in_view(&doc, config.scrolloff); - match event { - Event::Key(event) => match event.code { - KeyCode::Esc => { - self.sticky = None; - } - _ => { - // Temp solution - if self.apply_prompt { - if let Some(char) = event.char() { - if char == 'y' || char == 'Y' { - let (documents, count) = self.apply_refactor(cx.editor); - let result = format!( - "Refactored {} documents, {} lines changed.", - documents, count - ); - cx.editor.set_status(result); - return self.close(cx.editor); - } - } - cx.editor.set_status("Aborted"); - self.apply_prompt = false; - return EventResult::Consumed(None); - } - let sticky = self.sticky.clone(); - if let Some(key) = sticky.as_ref().and_then(|sticky| sticky.get(event)).or(self - .keymap - .get(&cx.editor.mode) - .and_then(|map| map.get(event))) - { - match key { - KeyTrie::Leaf(command) => { - if UNSUPPORTED_COMMANDS.contains(command.name()) { - cx.editor - .set_status("Command not supported in refactor view"); - return EventResult::Consumed(None); - } else if command.name() == "wclose" { - return self.close(cx.editor); - // TODO: custom command mode - } else if command.name() == "command_mode" { - cx.editor.set_status("Apply changes to documents? (y/n): "); - self.apply_prompt = true; - return EventResult::Consumed(None); - } - self.sticky = None; - cx.editor.autoinfo = None; - } - KeyTrie::Sequence(_) => (), - KeyTrie::Node(node) => { - self.sticky = Some(node.clone()); - cx.editor.autoinfo = Some(node.infobox()); - return EventResult::Consumed(None); - } - } - } - } - }, - _ => (), - } - EventResult::Ignored(None) - } - - fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) { - let view = view_mut!(cx.editor); - let view_area = area.clip_bottom(2); - view.area = view_area; - surface.clear_with(view_area, cx.editor.theme.get("ui.background")); - - self.render_view(&cx.editor, surface); - if cx.editor.config().auto_info { - if let Some(mut info) = cx.editor.autoinfo.take() { - info.render(area, surface, cx); - cx.editor.autoinfo = Some(info) - } - } - } -} diff --git a/helix-term/src/ui/statusline.rs b/helix-term/src/ui/statusline.rs index 501faea3963ed..037fbd69c816c 100644 --- a/helix-term/src/ui/statusline.rs +++ b/helix-term/src/ui/statusline.rs @@ -1,7 +1,7 @@ use helix_core::{coords_at_pos, encoding, Position}; use helix_lsp::lsp::DiagnosticSeverity; use helix_view::{ - document::{Mode, SCRATCH_BUFFER_NAME}, + document::{Mode, REFACTOR_BUFFER_NAME, SCRATCH_BUFFER_NAME}, graphics::Rect, theme::Style, Document, Editor, View, @@ -411,16 +411,24 @@ where F: Fn(&mut RenderContext, String, Option