From 724272931ad15fb27ff92f6a63763185bcfc84cc Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 6 Oct 2021 19:04:55 +0200 Subject: [PATCH] Skip autoclosed pairs Co-Authored-By: Nathan Sobo Co-Authored-By: Max Brunsfeld --- crates/editor/src/element.rs | 4 +- crates/editor/src/lib.rs | 206 ++++++++++++++++++++++++++-------- crates/file_finder/src/lib.rs | 8 +- crates/server/src/rpc.rs | 4 +- crates/workspace/src/lib.rs | 10 +- 5 files changed, 174 insertions(+), 58 deletions(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 76bab6a1fd..e3e48e475c 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -1,5 +1,5 @@ use super::{ - DisplayPoint, Editor, EditorMode, EditorSettings, EditorStyle, Insert, Scroll, Select, + DisplayPoint, Editor, EditorMode, EditorSettings, EditorStyle, Input, Scroll, Select, SelectPhase, Snapshot, MAX_LINE_LEN, }; use buffer::HighlightId; @@ -143,7 +143,7 @@ impl EditorElement { if chars.chars().any(|c| c.is_control()) || keystroke.cmd || keystroke.ctrl { false } else { - cx.dispatch_action(Insert(chars.to_string())); + cx.dispatch_action(Input(chars.to_string())); true } } diff --git a/crates/editor/src/lib.rs b/crates/editor/src/lib.rs index 25038468eb..856aa37799 100644 --- a/crates/editor/src/lib.rs +++ b/crates/editor/src/lib.rs @@ -37,7 +37,7 @@ const MAX_LINE_LEN: usize = 1024; action!(Cancel); action!(Backspace); action!(Delete); -action!(Insert, String); +action!(Input, String); action!(DeleteLine); action!(DeleteToPreviousWordBoundary); action!(DeleteToNextWordBoundary); @@ -95,13 +95,13 @@ pub fn init(cx: &mut MutableAppContext) { Binding::new("ctrl-h", Backspace, Some("Editor")), Binding::new("delete", Delete, Some("Editor")), Binding::new("ctrl-d", Delete, Some("Editor")), - Binding::new("enter", Insert("\n".into()), Some("Editor && mode == full")), + Binding::new("enter", Input("\n".into()), Some("Editor && mode == full")), Binding::new( "alt-enter", - Insert("\n".into()), + Input("\n".into()), Some("Editor && mode == auto_height"), ), - Binding::new("tab", Insert("\t".into()), Some("Editor")), + Binding::new("tab", Input("\t".into()), Some("Editor")), Binding::new("ctrl-shift-K", DeleteLine, Some("Editor")), Binding::new( "alt-backspace", @@ -192,7 +192,7 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_action(|this: &mut Editor, action: &Scroll, cx| this.set_scroll_position(action.0, cx)); cx.add_action(Editor::select); cx.add_action(Editor::cancel); - cx.add_action(Editor::insert); + cx.add_action(Editor::handle_input); cx.add_action(Editor::backspace); cx.add_action(Editor::delete); cx.add_action(Editor::delete_line); @@ -292,6 +292,7 @@ pub struct Editor { pending_selection: Option, next_selection_id: usize, add_selections_state: Option, + autoclose_stack: Vec, select_larger_syntax_node_stack: Vec>, scroll_position: Vector2F, scroll_top_anchor: Anchor, @@ -319,6 +320,11 @@ struct AddSelectionsState { stack: Vec, } +struct AutoclosePairState { + ranges: SmallVec<[Range; 32]>, + pair: AutoclosePair, +} + #[derive(Serialize, Deserialize)] struct ClipboardSelection { len: usize, @@ -404,6 +410,7 @@ impl Editor { pending_selection: None, next_selection_id, add_selections_state: None, + autoclose_stack: Default::default(), select_larger_syntax_node_stack: Vec::new(), build_settings, scroll_position: Vector2F::zero(), @@ -733,7 +740,18 @@ impl Editor { Ok(()) } - pub fn insert(&mut self, action: &Insert, cx: &mut ViewContext) { + pub fn handle_input(&mut self, action: &Input, cx: &mut ViewContext) { + let text = action.0.as_ref(); + if !self.skip_autoclose_end(text, cx) { + self.start_transaction(cx); + self.insert(text, cx); + self.autoclose_pairs(cx); + self.end_transaction(cx); + } + } + + fn insert(&mut self, text: &str, cx: &mut ViewContext) { + self.start_transaction(cx); let mut old_selections = SmallVec::<[_; 32]>::new(); { let selections = self.selections(cx); @@ -745,12 +763,11 @@ impl Editor { } } - self.start_transaction(cx); let mut new_selections = Vec::new(); self.buffer.update(cx, |buffer, cx| { let edit_ranges = old_selections.iter().map(|(_, range)| range.clone()); - buffer.edit(edit_ranges, action.0.as_str(), cx); - let text_len = action.0.len() as isize; + buffer.edit(edit_ranges, text, cx); + let text_len = text.len() as isize; let mut delta = 0_isize; new_selections = old_selections .into_iter() @@ -772,13 +789,12 @@ impl Editor { }); self.update_selections(new_selections, true, cx); - self.autoclose_pairs(cx); self.end_transaction(cx); } fn autoclose_pairs(&mut self, cx: &mut ViewContext) { let selections = self.selections(cx); - self.buffer.update(cx, |buffer, cx| { + let new_autoclose_pair_state = self.buffer.update(cx, |buffer, cx| { let autoclose_pair = buffer.language().and_then(|language| { let first_selection_start = selections.first().unwrap().start.to_offset(&*buffer); let pair = language.autoclose_pairs().iter().find(|pair| { @@ -804,23 +820,87 @@ impl Editor { }) }); - if let Some(pair) = autoclose_pair { - let mut selection_ranges = SmallVec::<[_; 32]>::new(); - for selection in selections.as_ref() { - let start = selection.start.to_offset(&*buffer); - let end = selection.end.to_offset(&*buffer); - selection_ranges.push(start..end); - } + autoclose_pair.and_then(|pair| { + let selection_ranges = selections + .iter() + .map(|selection| { + let start = selection.start.to_offset(&*buffer); + start..start + }) + .collect::>(); buffer.edit(selection_ranges, &pair.end, cx); - } + + if pair.end.len() == 1 { + Some(AutoclosePairState { + ranges: selections + .iter() + .map(|selection| { + selection.start.bias_left(buffer) + ..selection.start.bias_right(buffer) + }) + .collect(), + pair, + }) + } else { + None + } + }) }); + self.autoclose_stack.extend(new_autoclose_pair_state); + } + + fn skip_autoclose_end(&mut self, text: &str, cx: &mut ViewContext) -> bool { + let old_selections = self.selections(cx); + let autoclose_pair_state = if let Some(autoclose_pair_state) = self.autoclose_stack.last() { + autoclose_pair_state + } else { + return false; + }; + if text != autoclose_pair_state.pair.end { + return false; + } + + debug_assert_eq!(old_selections.len(), autoclose_pair_state.ranges.len()); + + let buffer = self.buffer.read(cx); + let old_selection_ranges: SmallVec<[_; 32]> = old_selections + .iter() + .map(|selection| (selection.id, selection.offset_range(buffer))) + .collect(); + if old_selection_ranges + .iter() + .zip(&autoclose_pair_state.ranges) + .all(|((_, selection_range), autoclose_range)| { + let autoclose_range_end = autoclose_range.end.to_offset(buffer); + selection_range.is_empty() && selection_range.start == autoclose_range_end + }) + { + let new_selections = old_selection_ranges + .into_iter() + .map(|(id, range)| { + let new_head = buffer.anchor_before(range.start + 1); + Selection { + id, + start: new_head.clone(), + end: new_head, + reversed: false, + goal: SelectionGoal::None, + } + }) + .collect(); + self.autoclose_stack.pop(); + self.update_selections(new_selections, true, cx); + true + } else { + false + } } pub fn clear(&mut self, cx: &mut ViewContext) { self.start_transaction(cx); self.select_all(&SelectAll, cx); - self.insert(&Insert(String::new()), cx); + self.insert("", cx); self.end_transaction(cx); } @@ -843,7 +923,7 @@ impl Editor { } self.update_selections(selections, true, cx); - self.insert(&Insert(String::new()), cx); + self.insert("", cx); self.end_transaction(cx); } @@ -866,7 +946,7 @@ impl Editor { } self.update_selections(selections, true, cx); - self.insert(&Insert(String::new()), cx); + self.insert(&"", cx); self.end_transaction(cx); } @@ -1214,7 +1294,7 @@ impl Editor { } } self.update_selections(selections, true, cx); - self.insert(&Insert(String::new()), cx); + self.insert("", cx); self.end_transaction(cx); cx.as_mut() @@ -1261,7 +1341,6 @@ impl Editor { clipboard_selections.clear(); } - self.start_transaction(cx); let mut start_offset = 0; let mut new_selections = Vec::with_capacity(selections.len()); for (i, selection) in selections.iter().enumerate() { @@ -1304,9 +1383,8 @@ impl Editor { }); } self.update_selections(new_selections, true, cx); - self.end_transaction(cx); } else { - self.insert(&Insert(clipboard_text.into()), cx); + self.insert(clipboard_text, cx); } } } @@ -1548,7 +1626,7 @@ impl Editor { } self.update_selections(selections, true, cx); - self.insert(&Insert(String::new()), cx); + self.insert("", cx); self.end_transaction(cx); } @@ -1618,7 +1696,7 @@ impl Editor { } self.update_selections(selections, true, cx); - self.insert(&Insert(String::new()), cx); + self.insert("", cx); self.end_transaction(cx); } @@ -2146,20 +2224,41 @@ impl Editor { } } - self.buffer.update(cx, |buffer, cx| { - buffer - .update_selection_set(self.selection_set_id, selections, cx) - .unwrap(); - }); - self.pause_cursor_blinking(cx); + self.add_selections_state = None; + self.select_larger_syntax_node_stack.clear(); + while let Some(autoclose_pair_state) = self.autoclose_stack.last() { + let all_selections_inside_autoclose_ranges = + if selections.len() == autoclose_pair_state.ranges.len() { + selections.iter().zip(&autoclose_pair_state.ranges).all( + |(selection, autoclose_range)| { + let head = selection.head(); + autoclose_range.start.cmp(head, buffer).unwrap() <= Ordering::Equal + && autoclose_range.end.cmp(head, buffer).unwrap() >= Ordering::Equal + }, + ) + } else { + false + }; + + if all_selections_inside_autoclose_ranges { + break; + } else { + self.autoclose_stack.pop(); + } + } if autoscroll { self.autoscroll_requested = true; cx.notify(); } - self.add_selections_state = None; - self.select_larger_syntax_node_stack.clear(); + self.pause_cursor_blinking(cx); + + self.buffer.update(cx, |buffer, cx| { + buffer + .update_selection_set(self.selection_set_id, selections, cx) + .unwrap(); + }); } fn start_transaction(&self, cx: &mut ViewContext) { @@ -3708,9 +3807,9 @@ mod tests { // is pasted at each cursor. view.update(cx, |view, cx| { view.select_ranges(vec![0..0, 31..31], false, cx); - view.insert(&Insert("( ".into()), cx); + view.handle_input(&Input("( ".into()), cx); view.paste(&Paste, cx); - view.insert(&Insert(") ".into()), cx); + view.handle_input(&Input(") ".into()), cx); assert_eq!( view.display_text(cx), "( one✅ three five ) two one✅ four three six five ( one✅ three five ) " @@ -3719,7 +3818,7 @@ mod tests { view.update(cx, |view, cx| { view.select_ranges(vec![0..0], false, cx); - view.insert(&Insert("123\n4567\n89\n".into()), cx); + view.handle_input(&Input("123\n4567\n89\n".into()), cx); assert_eq!( view.display_text(cx), "123\n4567\n89\n( one✅ three five ) two one✅ four three six five ( one✅ three five ) " @@ -4296,12 +4395,29 @@ mod tests { cx, ) .unwrap(); - view.insert(&Insert("{".to_string()), cx); + view.handle_input(&Input("{".to_string()), cx); + view.handle_input(&Input("{".to_string()), cx); + view.handle_input(&Input("{".to_string()), cx); assert_eq!( view.text(cx), " - {} - {} + {{{}}} + {{{}}} + / + + " + .unindent() + ); + + view.move_right(&MoveRight, cx); + view.handle_input(&Input("}".to_string()), cx); + view.handle_input(&Input("}".to_string()), cx); + view.handle_input(&Input("}".to_string()), cx); + assert_eq!( + view.text(cx), + " + {{{}}}} + {{{}}}} / " @@ -4309,8 +4425,8 @@ mod tests { ); view.undo(&Undo, cx); - view.insert(&Insert("/".to_string()), cx); - view.insert(&Insert("*".to_string()), cx); + view.handle_input(&Input("/".to_string()), cx); + view.handle_input(&Input("*".to_string()), cx); assert_eq!( view.text(cx), " @@ -4331,7 +4447,7 @@ mod tests { cx, ) .unwrap(); - view.insert(&Insert("*".to_string()), cx); + view.handle_input(&Input("*".to_string()), cx); assert_eq!( view.text(cx), " diff --git a/crates/file_finder/src/lib.rs b/crates/file_finder/src/lib.rs index 95c4ae9249..bd52023de8 100644 --- a/crates/file_finder/src/lib.rs +++ b/crates/file_finder/src/lib.rs @@ -422,7 +422,7 @@ impl FileFinder { #[cfg(test)] mod tests { use super::*; - use editor::Insert; + use editor::Input; use serde_json::json; use std::path::PathBuf; use workspace::{Workspace, WorkspaceParams}; @@ -471,9 +471,9 @@ mod tests { let query_buffer = cx.read(|cx| finder.read(cx).query_editor.clone()); let chain = vec![finder.id(), query_buffer.id()]; - cx.dispatch_action(window_id, chain.clone(), Insert("b".into())); - cx.dispatch_action(window_id, chain.clone(), Insert("n".into())); - cx.dispatch_action(window_id, chain.clone(), Insert("a".into())); + cx.dispatch_action(window_id, chain.clone(), Input("b".into())); + cx.dispatch_action(window_id, chain.clone(), Input("n".into())); + cx.dispatch_action(window_id, chain.clone(), Input("a".into())); finder .condition(&cx, |finder, _| finder.matches.len() == 2) .await; diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index 1de7d68185..147a655f72 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -981,7 +981,7 @@ mod tests { self, test::FakeHttpClient, Channel, ChannelDetails, ChannelList, Client, Credentials, EstablishConnectionError, UserStore, }, - editor::{Editor, EditorSettings, Insert}, + editor::{Editor, EditorSettings, Input}, fs::{FakeFs, Fs as _}, people_panel::JoinWorktree, project::{ProjectPath, Worktree}, @@ -1068,7 +1068,7 @@ mod tests { // Edit the buffer as client B and see that edit as client A. editor_b.update(&mut cx_b, |editor, cx| { - editor.insert(&Insert("ok, ".into()), cx) + editor.handle_input(&Input("ok, ".into()), cx) }); buffer_a .condition(&cx_a, |buffer, _| buffer.text() == "ok, b-contents") diff --git a/crates/workspace/src/lib.rs b/crates/workspace/src/lib.rs index 6a0b7c0031..c227ee61bd 100644 --- a/crates/workspace/src/lib.rs +++ b/crates/workspace/src/lib.rs @@ -1070,7 +1070,7 @@ impl WorkspaceHandle for ViewHandle { #[cfg(test)] mod tests { use super::*; - use editor::{Editor, Insert}; + use editor::{Editor, Input}; use serde_json::json; use std::collections::HashSet; @@ -1282,7 +1282,7 @@ mod tests { item.to_any().downcast::().unwrap() }); - cx.update(|cx| editor.update(cx, |editor, cx| editor.insert(&Insert("x".into()), cx))); + cx.update(|cx| editor.update(cx, |editor, cx| editor.handle_input(&Input("x".into()), cx))); fs.insert_file("/root/a.txt", "changed".to_string()) .await .unwrap(); @@ -1335,7 +1335,7 @@ mod tests { assert!(!editor.is_dirty(cx.as_ref())); assert_eq!(editor.title(cx.as_ref()), "untitled"); assert!(editor.language(cx).is_none()); - editor.insert(&Insert("hi".into()), cx); + editor.handle_input(&Input("hi".into()), cx); assert!(editor.is_dirty(cx.as_ref())); }); @@ -1367,7 +1367,7 @@ mod tests { // Edit the file and save it again. This time, there is no filename prompt. editor.update(&mut cx, |editor, cx| { - editor.insert(&Insert(" there".into()), cx); + editor.handle_input(&Input(" there".into()), cx); assert_eq!(editor.is_dirty(cx.as_ref()), true); }); workspace.update(&mut cx, |workspace, cx| { @@ -1428,7 +1428,7 @@ mod tests { editor.update(&mut cx, |editor, cx| { assert!(editor.language(cx).is_none()); - editor.insert(&Insert("hi".into()), cx); + editor.handle_input(&Input("hi".into()), cx); assert!(editor.is_dirty(cx.as_ref())); });