From 2b0794f5ae76d1f1fdd6a8d16c3a0db17ddc9cb6 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 9 Sep 2022 17:40:34 -0700 Subject: [PATCH] Restructure autoclosing to account for multi-language documents --- crates/editor/src/editor.rs | 570 +++++++++++----------- crates/zed/src/languages/html/config.toml | 2 +- 2 files changed, 280 insertions(+), 292 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index b313d2de55214eba8463a60d269e26fbceba4e67..055b73d7dc6e8f6817bc9742d664dff6ccab5357 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -410,7 +410,7 @@ pub struct Editor { add_selections_state: Option, select_next_state: Option, selection_history: SelectionHistory, - autoclose_stack: InvalidationStack, + autoclose_regions: Vec, snippet_stack: InvalidationStack, select_larger_syntax_node_stack: Vec]>>, ime_transaction: Option, @@ -569,8 +569,9 @@ struct SelectNextState { done: bool, } -struct BracketPairState { - ranges: Vec>, +struct AutocloseRegion { + selection_id: usize, + range: Range, pair: BracketPair, } @@ -1010,7 +1011,7 @@ impl Editor { add_selections_state: None, select_next_state: None, selection_history: Default::default(), - autoclose_stack: Default::default(), + autoclose_regions: Default::default(), snippet_stack: Default::default(), select_larger_syntax_node_stack: Vec::new(), ime_transaction: Default::default(), @@ -1401,8 +1402,7 @@ impl Editor { self.add_selections_state = None; self.select_next_state = None; self.select_larger_syntax_node_stack.clear(); - self.autoclose_stack - .invalidate(&self.selections.disjoint_anchors(), buffer); + self.invalidate_autoclose_regions(&self.selections.disjoint_anchors(), buffer); self.snippet_stack .invalidate(&self.selections.disjoint_anchors(), buffer); self.take_rename(false, cx); @@ -1849,15 +1849,158 @@ impl Editor { return; } - if !self.skip_autoclose_end(text, cx) { - self.transact(cx, |this, cx| { - if !this.surround_with_bracket_pair(text, cx) { - this.insert(text, cx); - this.autoclose_bracket_pairs(cx); + let text: Arc = text.into(); + let selections = self.selections.all_adjusted(cx); + let mut edits = Vec::new(); + let mut new_selections = Vec::with_capacity(selections.len()); + let mut new_autoclose_regions = Vec::new(); + let snapshot = self.buffer.read(cx).read(cx); + + for (selection, autoclose_region) in + self.selections_with_autoclose_regions(selections, &snapshot) + { + if let Some(language) = snapshot.language_at(selection.head()) { + // Determine if the inserted text matches the opening or closing + // bracket of any of this language's bracket pairs. + let mut bracket_pair = None; + let mut is_bracket_pair_start = false; + for pair in language.brackets() { + if pair.start.ends_with(text.as_ref()) { + bracket_pair = Some(pair.clone()); + is_bracket_pair_start = true; + break; + } else if pair.end.as_str() == text.as_ref() { + bracket_pair = Some(pair.clone()); + break; + } } - }); - self.trigger_completion_on_input(text, cx); + + if let Some(bracket_pair) = bracket_pair { + if selection.is_empty() { + if is_bracket_pair_start { + let prefix_len = bracket_pair.start.len() - text.len(); + + // If the inserted text is a suffix of an opening bracket and the + // selection is preceded by the rest of the opening bracket, then + // insert the closing bracket. + let should_autoclose = selection.start.column > (prefix_len as u32) + && snapshot.contains_str_at( + Point::new( + selection.start.row, + selection.start.column - (prefix_len as u32), + ), + &bracket_pair.start[..prefix_len], + ) + && snapshot + .chars_at(selection.start) + .next() + .map_or(true, |c| language.should_autoclose_before(c)); + if should_autoclose { + let anchor = snapshot.anchor_before(selection.end); + new_selections + .push((selection.map(|_| anchor.clone()), text.len())); + new_autoclose_regions.push(( + anchor.clone(), + text.len(), + selection.id, + bracket_pair.clone(), + )); + edits.push(( + selection.range(), + format!("{}{}", text, bracket_pair.end).into(), + )); + continue; + } + } else if let Some(region) = autoclose_region { + // If the selection is followed by an auto-inserted closing bracket, + // then don't insert anything else; just move the selection past the + // closing bracket. + let should_skip = selection.end == region.range.end.to_point(&snapshot); + if should_skip { + let anchor = snapshot.anchor_after(selection.end); + new_selections.push(( + selection.map(|_| anchor.clone()), + region.pair.end.len(), + )); + continue; + } + } + } + // If an opening bracket is typed while text is selected, then + // surround that text with the bracket pair. + else if is_bracket_pair_start { + edits.push((selection.start..selection.start, text.clone())); + edits.push(( + selection.end..selection.end, + bracket_pair.end.as_str().into(), + )); + new_selections.push(( + Selection { + id: selection.id, + start: snapshot.anchor_after(selection.start), + end: snapshot.anchor_before(selection.end), + reversed: selection.reversed, + goal: selection.goal, + }, + 0, + )); + continue; + } + } + } + + // If not handling any auto-close operation, then just replace the selected + // text with the given input and move the selection to the end of the + // newly inserted text. + let anchor = snapshot.anchor_after(selection.end); + new_selections.push((selection.map(|_| anchor.clone()), 0)); + edits.push((selection.start..selection.end, text.clone())); } + + drop(snapshot); + self.transact(cx, |this, cx| { + this.buffer.update(cx, |buffer, cx| { + buffer.edit(edits, Some(AutoindentMode::EachLine), cx); + }); + + let new_anchor_selections = new_selections.iter().map(|e| &e.0); + let new_selection_deltas = new_selections.iter().map(|e| e.1); + let snapshot = this.buffer.read(cx).read(cx); + let new_selections = resolve_multiple::(new_anchor_selections, &snapshot) + .zip(new_selection_deltas) + .map(|(selection, delta)| selection.map(|e| e + delta)) + .collect::>(); + + let mut i = 0; + for (position, delta, selection_id, pair) in new_autoclose_regions { + let position = position.to_offset(&snapshot) + delta; + let start = snapshot.anchor_before(position); + let end = snapshot.anchor_after(position); + while let Some(existing_state) = this.autoclose_regions.get(i) { + match existing_state.range.start.cmp(&start, &snapshot) { + Ordering::Less => i += 1, + Ordering::Greater => break, + Ordering::Equal => match end.cmp(&existing_state.range.end, &snapshot) { + Ordering::Less => i += 1, + Ordering::Equal => break, + Ordering::Greater => break, + }, + } + } + this.autoclose_regions.insert( + i, + AutocloseRegion { + selection_id, + range: start..end, + pair, + }, + ); + } + + drop(snapshot); + this.change_selections(None, cx, |s| s.select(new_selections)); + this.trigger_completion_on_input(&text, cx); + }); } pub fn newline(&mut self, _: &Newline, cx: &mut ViewContext) { @@ -2029,232 +2172,89 @@ impl Editor { } } - fn surround_with_bracket_pair(&mut self, text: &str, cx: &mut ViewContext) -> bool { - let snapshot = self.buffer.read(cx).snapshot(cx); - if let Some(pair) = snapshot - .language() - .and_then(|language| language.brackets().iter().find(|b| b.start == text)) - .cloned() - { - if self - .selections - .all::(cx) - .iter() - .any(|selection| selection.is_empty()) - { - return false; - } - - let mut selections = self.selections.disjoint_anchors().to_vec(); - for selection in &mut selections { - selection.end = selection.end.bias_left(&snapshot); - } - drop(snapshot); - - self.buffer.update(cx, |buffer, cx| { - let pair_start: Arc = pair.start.clone().into(); - let pair_end: Arc = pair.end.clone().into(); - buffer.edit( - selections.iter().flat_map(|s| { - [ - (s.start.clone()..s.start.clone(), pair_start.clone()), - (s.end.clone()..s.end.clone(), pair_end.clone()), - ] - }), - None, - cx, - ); - }); - - let snapshot = self.buffer.read(cx).read(cx); - for selection in &mut selections { - selection.end = selection.end.bias_right(&snapshot); - } - drop(snapshot); - - self.change_selections(None, cx, |s| s.select_anchors(selections)); - true - } else { - false - } - } - - fn autoclose_bracket_pairs(&mut self, cx: &mut ViewContext) { + /// If any empty selections is touching the start of its innermost containing autoclose + /// region, expand it to select the brackets. + fn select_autoclose_pair(&mut self, cx: &mut ViewContext) { let selections = self.selections.all::(cx); - let mut bracket_pair_state = None; - let mut new_selections = None; - self.buffer.update(cx, |buffer, cx| { - let mut snapshot = buffer.snapshot(cx); - let left_biased_selections = selections - .iter() - .map(|selection| selection.map(|p| snapshot.anchor_before(p))) - .collect::>(); - - let autoclose_pair = snapshot.language().and_then(|language| { - let first_selection_start = selections.first().unwrap().start; - let pair = language.brackets().iter().find(|pair| { - pair.close - && snapshot.contains_str_at( - first_selection_start.saturating_sub(pair.start.len()), - &pair.start, - ) - }); - pair.and_then(|pair| { - let should_autoclose = selections.iter().all(|selection| { - // Ensure all selections are parked at the end of a pair start. - if snapshot.contains_str_at( - selection.start.saturating_sub(pair.start.len()), - &pair.start, - ) { - snapshot - .chars_at(selection.start) - .next() - .map_or(true, |c| language.should_autoclose_before(c)) - } else { - false + let buffer = self.buffer.read(cx).read(cx); + let mut new_selections = Vec::new(); + for (mut selection, region) in self.selections_with_autoclose_regions(selections, &buffer) { + if let (Some(region), true) = (region, selection.is_empty()) { + let mut range = region.range.to_offset(&buffer); + if selection.start == range.start { + if range.start >= region.pair.start.len() { + range.start -= region.pair.start.len(); + if buffer.contains_str_at(range.start, ®ion.pair.start) { + if buffer.contains_str_at(range.end, ®ion.pair.end) { + range.end += region.pair.end.len(); + selection.start = range.start; + selection.end = range.end; + } } - }); - - if should_autoclose { - Some(pair.clone()) - } else { - None } - }) - }); - - if let Some(pair) = autoclose_pair { - let selection_ranges = selections - .iter() - .map(|selection| { - let start = selection.start.to_offset(&snapshot); - start..start - }) - .collect::>(); - - let pair_end: Arc = pair.end.clone().into(); - buffer.edit( - selection_ranges - .iter() - .map(|range| (range.clone(), pair_end.clone())), - None, - cx, - ); - snapshot = buffer.snapshot(cx); - - new_selections = Some( - resolve_multiple::(left_biased_selections.iter(), &snapshot) - .collect::>(), - ); - - if pair.end.len() == 1 { - let mut delta = 0; - bracket_pair_state = Some(BracketPairState { - ranges: selections - .iter() - .map(move |selection| { - let offset = selection.start + delta; - delta += 1; - snapshot.anchor_before(offset)..snapshot.anchor_after(offset) - }) - .collect(), - pair, - }); } } - }); - - if let Some(new_selections) = new_selections { - self.change_selections(None, cx, |s| { - s.select(new_selections); - }); - } - if let Some(bracket_pair_state) = bracket_pair_state { - self.autoclose_stack.push(bracket_pair_state); - } - } - - fn skip_autoclose_end(&mut self, text: &str, cx: &mut ViewContext) -> bool { - let buffer = self.buffer.read(cx).snapshot(cx); - let old_selections = self.selections.all::(cx); - let autoclose_pair = if let Some(autoclose_pair) = self.autoclose_stack.last() { - autoclose_pair - } else { - return false; - }; - if text != autoclose_pair.pair.end { - return false; + new_selections.push(selection); } - debug_assert_eq!(old_selections.len(), autoclose_pair.ranges.len()); - - if old_selections - .iter() - .zip(autoclose_pair.ranges.iter().map(|r| r.to_offset(&buffer))) - .all(|(selection, autoclose_range)| { - let autoclose_range_end = autoclose_range.end.to_offset(&buffer); - selection.is_empty() && selection.start == autoclose_range_end - }) - { - let new_selections = old_selections - .into_iter() - .map(|selection| { - let cursor = selection.start + 1; - Selection { - id: selection.id, - start: cursor, - end: cursor, - reversed: false, - goal: SelectionGoal::None, - } - }) - .collect(); - self.autoclose_stack.pop(); - self.change_selections(Some(Autoscroll::Fit), cx, |s| { - s.select(new_selections); - }); - true - } else { - false - } + drop(buffer); + self.change_selections(None, cx, |selections| selections.select(new_selections)); } - fn select_autoclose_pair(&mut self, cx: &mut ViewContext) -> bool { - let buffer = self.buffer.read(cx).snapshot(cx); - let old_selections = self.selections.all::(cx); - let autoclose_pair = if let Some(autoclose_pair) = self.autoclose_stack.last() { - autoclose_pair - } else { - return false; - }; + /// Iterate the given selections, and for each one, find the smallest surrounding + /// autoclose region. This uses the ordering of the selections and the autoclose + /// regions to avoid repeated comparisons. + fn selections_with_autoclose_regions<'a, D: ToOffset + Clone>( + &'a self, + selections: impl IntoIterator>, + buffer: &'a MultiBufferSnapshot, + ) -> impl Iterator, Option<&'a AutocloseRegion>)> { + let mut i = 0; + let mut pair_states = self.autoclose_regions.as_slice(); + selections.into_iter().map(move |selection| { + let range = selection.start.to_offset(buffer)..selection.end.to_offset(buffer); + + let mut enclosing = None; + while let Some(pair_state) = pair_states.get(i) { + if pair_state.range.end.to_offset(buffer) < range.start { + pair_states = &pair_states[i + 1..]; + i = 0; + } else if pair_state.range.start.to_offset(buffer) > range.end { + break; + } else if pair_state.selection_id == selection.id { + enclosing = Some(pair_state); + i += 1; + } + } - debug_assert_eq!(old_selections.len(), autoclose_pair.ranges.len()); + (selection.clone(), enclosing) + }) + } - let mut new_selections = Vec::new(); - for (selection, autoclose_range) in old_selections - .iter() - .zip(autoclose_pair.ranges.iter().map(|r| r.to_offset(&buffer))) - { - if selection.is_empty() - && autoclose_range.is_empty() - && selection.start == autoclose_range.start - { - new_selections.push(Selection { - id: selection.id, - start: selection.start - autoclose_pair.pair.start.len(), - end: selection.end + autoclose_pair.pair.end.len(), - reversed: true, - goal: selection.goal, - }); - } else { - return false; + /// Remove any autoclose regions that no longer contain their selection. + fn invalidate_autoclose_regions( + &mut self, + mut selections: &[Selection], + buffer: &MultiBufferSnapshot, + ) { + self.autoclose_regions.retain(|state| { + let mut i = 0; + while let Some(selection) = selections.get(i) { + if selection.end.cmp(&state.range.start, buffer).is_lt() { + selections = &selections[1..]; + continue; + } + if selection.start.cmp(&state.range.end, buffer).is_gt() { + break; + } + if selection.id == state.selection_id { + return true; + } else { + i += 1; + } } - } - - self.change_selections(Some(Autoscroll::Fit), cx, |selections| { - selections.select(new_selections) + false }); - true } fn completion_query(buffer: &MultiBufferSnapshot, position: impl ToOffset) -> Option { @@ -2909,51 +2909,47 @@ impl Editor { pub fn backspace(&mut self, _: &Backspace, cx: &mut ViewContext) { self.transact(cx, |this, cx| { - if !this.select_autoclose_pair(cx) { - let mut selections = this.selections.all::(cx); - if !this.selections.line_mode { - let display_map = this.display_map.update(cx, |map, cx| map.snapshot(cx)); - for selection in &mut selections { - if selection.is_empty() { - let old_head = selection.head(); - let mut new_head = movement::left( - &display_map, - old_head.to_display_point(&display_map), - ) - .to_point(&display_map); - if let Some((buffer, line_buffer_range)) = display_map - .buffer_snapshot - .buffer_line_for_row(old_head.row) - { - let indent_size = - buffer.indent_size_for_line(line_buffer_range.start.row); - let language_name = - buffer.language().map(|language| language.name()); - let indent_len = match indent_size.kind { - IndentKind::Space => { - cx.global::().tab_size(language_name.as_deref()) - } - IndentKind::Tab => NonZeroU32::new(1).unwrap(), - }; - if old_head.column <= indent_size.len && old_head.column > 0 { - let indent_len = indent_len.get(); - new_head = cmp::min( - new_head, - Point::new( - old_head.row, - ((old_head.column - 1) / indent_len) * indent_len, - ), - ); + this.select_autoclose_pair(cx); + let mut selections = this.selections.all::(cx); + if !this.selections.line_mode { + let display_map = this.display_map.update(cx, |map, cx| map.snapshot(cx)); + for selection in &mut selections { + if selection.is_empty() { + let old_head = selection.head(); + let mut new_head = + movement::left(&display_map, old_head.to_display_point(&display_map)) + .to_point(&display_map); + if let Some((buffer, line_buffer_range)) = display_map + .buffer_snapshot + .buffer_line_for_row(old_head.row) + { + let indent_size = + buffer.indent_size_for_line(line_buffer_range.start.row); + let language_name = buffer.language().map(|language| language.name()); + let indent_len = match indent_size.kind { + IndentKind::Space => { + cx.global::().tab_size(language_name.as_deref()) } + IndentKind::Tab => NonZeroU32::new(1).unwrap(), + }; + if old_head.column <= indent_size.len && old_head.column > 0 { + let indent_len = indent_len.get(); + new_head = cmp::min( + new_head, + Point::new( + old_head.row, + ((old_head.column - 1) / indent_len) * indent_len, + ), + ); } - - selection.set_head(new_head, SelectionGoal::None); } + + selection.set_head(new_head, SelectionGoal::None); } } - - this.change_selections(Some(Autoscroll::Fit), cx, |s| s.select(selections)); } + + this.change_selections(Some(Autoscroll::Fit), cx, |s| s.select(selections)); this.insert("", cx); }); } @@ -3957,17 +3953,16 @@ impl Editor { cx: &mut ViewContext, ) { self.transact(cx, |this, cx| { - if !this.select_autoclose_pair(cx) { - this.change_selections(Some(Autoscroll::Fit), cx, |s| { - let line_mode = s.line_mode; - s.move_with(|map, selection| { - if selection.is_empty() && !line_mode { - let cursor = movement::previous_word_start(map, selection.head()); - selection.set_head(cursor, SelectionGoal::None); - } - }); + this.select_autoclose_pair(cx); + this.change_selections(Some(Autoscroll::Fit), cx, |s| { + let line_mode = s.line_mode; + s.move_with(|map, selection| { + if selection.is_empty() && !line_mode { + let cursor = movement::previous_word_start(map, selection.head()); + selection.set_head(cursor, SelectionGoal::None); + } }); - } + }); this.insert("", cx); }); } @@ -3978,17 +3973,16 @@ impl Editor { cx: &mut ViewContext, ) { self.transact(cx, |this, cx| { - if !this.select_autoclose_pair(cx) { - this.change_selections(Some(Autoscroll::Fit), cx, |s| { - let line_mode = s.line_mode; - s.move_with(|map, selection| { - if selection.is_empty() && !line_mode { - let cursor = movement::previous_subword_start(map, selection.head()); - selection.set_head(cursor, SelectionGoal::None); - } - }); + this.select_autoclose_pair(cx); + this.change_selections(Some(Autoscroll::Fit), cx, |s| { + let line_mode = s.line_mode; + s.move_with(|map, selection| { + if selection.is_empty() && !line_mode { + let cursor = movement::previous_subword_start(map, selection.head()); + selection.set_head(cursor, SelectionGoal::None); + } }); - } + }); this.insert("", cx); }); } @@ -6495,12 +6489,6 @@ impl DerefMut for InvalidationStack { } } -impl InvalidationRegion for BracketPairState { - fn ranges(&self) -> &[Range] { - &self.ranges - } -} - impl InvalidationRegion for SnippetState { fn ranges(&self) -> &[Range] { &self.ranges[self.active_index] diff --git a/crates/zed/src/languages/html/config.toml b/crates/zed/src/languages/html/config.toml index 0680717b2cda9c11134d69eedf9563ac010f8cc9..80b33b1243f6fdb0721baa6b51d3c4e57fb68fe3 100644 --- a/crates/zed/src/languages/html/config.toml +++ b/crates/zed/src/languages/html/config.toml @@ -1,6 +1,6 @@ name = "HTML" path_suffixes = ["html"] -autoclose_before = ">" +autoclose_before = ">})" brackets = [ { start = "<", end = ">", close = true, newline = true }, { start = "{", end = "}", close = true, newline = true },