Detailed changes
@@ -62,6 +62,7 @@ mod code_actions;
mod completions;
mod config;
mod diagnostics;
+mod input;
mod rewrap;
mod selection;
@@ -3451,14 +3452,6 @@ impl Editor {
}
}
- pub fn set_input_enabled(&mut self, input_enabled: bool) {
- self.input_enabled = input_enabled;
- }
-
- pub fn set_expects_character_input(&mut self, expects_character_input: bool) {
- self.expects_character_input = expects_character_input;
- }
-
pub fn set_edit_predictions_hidden_for_vim_mode(
&mut self,
hidden: bool,
@@ -3479,14 +3472,6 @@ impl Editor {
self.menu_edit_predictions_policy = value;
}
- pub fn set_autoindent(&mut self, autoindent: bool) {
- if autoindent {
- self.autoindent_mode = Some(AutoindentMode::EachLine);
- } else {
- self.autoindent_mode = None;
- }
- }
-
pub fn capability(&self, cx: &App) -> Capability {
if self.read_only {
Capability::ReadOnly
@@ -3503,22 +3488,10 @@ impl Editor {
self.read_only = read_only;
}
- pub fn set_use_autoclose(&mut self, autoclose: bool) {
- self.use_autoclose = autoclose;
- }
-
pub fn set_use_selection_highlight(&mut self, highlight: bool) {
self.use_selection_highlight = highlight;
}
- pub fn set_use_auto_surround(&mut self, auto_surround: bool) {
- self.use_auto_surround = auto_surround;
- }
-
- pub fn set_auto_replace_emoji_shortcode(&mut self, auto_replace: bool) {
- self.auto_replace_emoji_shortcode = auto_replace;
- }
-
pub fn set_should_serialize(&mut self, should_serialize: bool, cx: &App) {
self.buffer_serialization = should_serialize.then(|| {
BufferSerialization::new(
@@ -3677,1254 +3650,35 @@ impl Editor {
pub fn dismiss_menus_and_popups(
&mut self,
is_user_requested: bool,
- window: &mut Window,
- cx: &mut Context<Self>,
- ) -> bool {
- let mut dismissed = false;
-
- dismissed |= self.take_rename(false, window, cx).is_some();
- dismissed |= self.hide_blame_popover(true, cx);
- dismissed |= hide_hover(self, cx);
- dismissed |= self.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
- dismissed |= self.hide_context_menu(window, cx).is_some();
- dismissed |= self.mouse_context_menu.take().is_some();
- dismissed |= is_user_requested
- && self.discard_edit_prediction(EditPredictionDiscardReason::Rejected, cx);
- dismissed |= self.snippet_stack.pop().is_some();
- if self.diff_review_drag_state.is_some() {
- self.cancel_diff_review_drag(cx);
- dismissed = true;
- }
- if !self.diff_review_overlays.is_empty() {
- self.dismiss_all_diff_review_overlays(cx);
- dismissed = true;
- }
-
- if self.mode.is_full() && self.has_active_diagnostic_group() {
- self.dismiss_diagnostics(cx);
- dismissed = true;
- }
-
- dismissed
- }
-
- fn linked_editing_ranges_for(
- &self,
- query_range: Range<text::Anchor>,
- cx: &App,
- ) -> Option<HashMap<Entity<Buffer>, Vec<Range<text::Anchor>>>> {
- use text::ToOffset as TO;
-
- if self.linked_edit_ranges.is_empty() {
- return None;
- }
- if query_range.start.buffer_id != query_range.end.buffer_id {
- return None;
- };
- let multibuffer_snapshot = self.buffer.read(cx).snapshot(cx);
- let buffer = self.buffer.read(cx).buffer(query_range.end.buffer_id)?;
- let buffer_snapshot = buffer.read(cx).snapshot();
- let (base_range, linked_ranges) = self.linked_edit_ranges.get(
- buffer_snapshot.remote_id(),
- query_range.clone(),
- &buffer_snapshot,
- )?;
- // find offset from the start of current range to current cursor position
- let start_byte_offset = TO::to_offset(&base_range.start, &buffer_snapshot);
-
- let start_offset = TO::to_offset(&query_range.start, &buffer_snapshot);
- let start_difference = start_offset - start_byte_offset;
- let end_offset = TO::to_offset(&query_range.end, &buffer_snapshot);
- let end_difference = end_offset - start_byte_offset;
-
- // Current range has associated linked ranges.
- let mut linked_edits = HashMap::<_, Vec<_>>::default();
- for range in linked_ranges.iter() {
- let start_offset = TO::to_offset(&range.start, &buffer_snapshot);
- let end_offset = start_offset + end_difference;
- let start_offset = start_offset + start_difference;
- if start_offset > buffer_snapshot.len() || end_offset > buffer_snapshot.len() {
- continue;
- }
- if self.selections.disjoint_anchor_ranges().any(|s| {
- let Some((selection_start, _)) =
- multibuffer_snapshot.anchor_to_buffer_anchor(s.start)
- else {
- return false;
- };
- let Some((selection_end, _)) = multibuffer_snapshot.anchor_to_buffer_anchor(s.end)
- else {
- return false;
- };
- if selection_start.buffer_id != query_range.start.buffer_id
- || selection_end.buffer_id != query_range.end.buffer_id
- {
- return false;
- }
- TO::to_offset(&selection_start, &buffer_snapshot) <= end_offset
- && TO::to_offset(&selection_end, &buffer_snapshot) >= start_offset
- }) {
- continue;
- }
- let start = buffer_snapshot.anchor_after(start_offset);
- let end = buffer_snapshot.anchor_after(end_offset);
- linked_edits
- .entry(buffer.clone())
- .or_default()
- .push(start..end);
- }
- Some(linked_edits)
- }
-
- pub fn handle_input(&mut self, text: &str, window: &mut Window, cx: &mut Context<Self>) {
- let text: Arc<str> = text.into();
-
- if self.read_only(cx) {
- return;
- }
-
- self.unfold_buffers_with_selections(cx);
-
- let selections = self.selections.all_adjusted(&self.display_snapshot(cx));
- let mut bracket_inserted = false;
- let mut edits = Vec::new();
- let mut linked_edits = LinkedEdits::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);
- let mut clear_linked_edit_ranges = false;
- let mut all_selections_read_only = true;
- let mut has_adjacent_edits = false;
- let mut in_adjacent_group = false;
-
- let mut regions = self
- .selections_with_autoclose_regions(selections, &snapshot)
- .peekable();
-
- while let Some((selection, autoclose_region)) = regions.next() {
- if snapshot
- .point_to_buffer_point(selection.head())
- .is_none_or(|(snapshot, ..)| !snapshot.capability.editable())
- {
- continue;
- }
- if snapshot
- .point_to_buffer_point(selection.tail())
- .is_none_or(|(snapshot, ..)| !snapshot.capability.editable())
- {
- // note, ideally we'd clip the tail to the closest writeable region towards the head
- continue;
- }
- all_selections_read_only = false;
-
- if let Some(scope) = snapshot.language_scope_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;
- let mut is_bracket_pair_end = false;
- if !text.is_empty() {
- let mut bracket_pair_matching_end = None;
- // `text` can be empty when a user is using IME (e.g. Chinese Wubi Simplified)
- // and they are removing the character that triggered IME popup.
- for (pair, enabled) in scope.brackets() {
- if !pair.close && !pair.surround {
- continue;
- }
-
- if enabled && pair.start.ends_with(text.as_ref()) {
- let prefix_len = pair.start.len() - text.len();
- let preceding_text_matches_prefix = prefix_len == 0
- || (selection.start.column >= (prefix_len as u32)
- && snapshot.contains_str_at(
- Point::new(
- selection.start.row,
- selection.start.column - (prefix_len as u32),
- ),
- &pair.start[..prefix_len],
- ));
- if preceding_text_matches_prefix {
- bracket_pair = Some(pair.clone());
- is_bracket_pair_start = true;
- break;
- }
- }
- if pair.end.as_str() == text.as_ref() && bracket_pair_matching_end.is_none()
- {
- // take first bracket pair matching end, but don't break in case a later bracket
- // pair matches start
- bracket_pair_matching_end = Some(pair.clone());
- }
- }
- if let Some(end) = bracket_pair_matching_end
- && bracket_pair.is_none()
- {
- bracket_pair = Some(end);
- is_bracket_pair_end = true;
- }
- }
-
- if let Some(bracket_pair) = bracket_pair {
- let snapshot_settings = snapshot.language_settings_at(selection.start, cx);
- let autoclose = self.use_autoclose && snapshot_settings.use_autoclose;
- let auto_surround =
- self.use_auto_surround && snapshot_settings.use_auto_surround;
- if selection.is_empty() {
- if is_bracket_pair_start {
- // 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 following_text_allows_autoclose = snapshot
- .chars_at(selection.start)
- .next()
- .is_none_or(|c| scope.should_autoclose_before(c));
-
- let preceding_text_allows_autoclose = selection.start.column == 0
- || snapshot
- .reversed_chars_at(selection.start)
- .next()
- .is_none_or(|c| {
- bracket_pair.start != bracket_pair.end
- || !snapshot
- .char_classifier_at(selection.start)
- .is_word(c)
- });
-
- let is_closing_quote = if bracket_pair.end == bracket_pair.start
- && bracket_pair.start.len() == 1
- {
- let target = bracket_pair.start.chars().next().unwrap();
- let mut byte_offset = 0u32;
- let current_line_count = snapshot
- .reversed_chars_at(selection.start)
- .take_while(|&c| c != '\n')
- .filter(|c| {
- byte_offset += c.len_utf8() as u32;
- if *c != target {
- return false;
- }
-
- let point = Point::new(
- selection.start.row,
- selection.start.column.saturating_sub(byte_offset),
- );
-
- let is_enabled = snapshot
- .language_scope_at(point)
- .and_then(|scope| {
- scope
- .brackets()
- .find(|(pair, _)| {
- pair.start == bracket_pair.start
- })
- .map(|(_, enabled)| enabled)
- })
- .unwrap_or(true);
-
- let is_delimiter = snapshot
- .language_scope_at(Point::new(
- point.row,
- point.column + 1,
- ))
- .and_then(|scope| {
- scope
- .brackets()
- .find(|(pair, _)| {
- pair.start == bracket_pair.start
- })
- .map(|(_, enabled)| !enabled)
- })
- .unwrap_or(false);
-
- is_enabled && !is_delimiter
- })
- .count();
- current_line_count % 2 == 1
- } else {
- false
- };
-
- if autoclose
- && bracket_pair.close
- && following_text_allows_autoclose
- && preceding_text_allows_autoclose
- && !is_closing_quote
- {
- let anchor = snapshot.anchor_before(selection.end);
- new_selections.push((selection.map(|_| anchor), text.len()));
- new_autoclose_regions.push((
- anchor,
- text.len(),
- selection.id,
- bracket_pair.clone(),
- ));
- edits.push((
- selection.range(),
- format!("{}{}", text, bracket_pair.end).into(),
- ));
- bracket_inserted = true;
- continue;
- }
- }
-
- if let Some(region) = autoclose_region {
- // If the selection is followed by an auto-inserted closing bracket,
- // then don't insert that closing bracket again; just move the selection
- // past the closing bracket.
- let should_skip = selection.end == region.range.end.to_point(&snapshot)
- && text.as_ref() == region.pair.end.as_str()
- && snapshot.contains_str_at(region.range.end, text.as_ref());
- if should_skip {
- let anchor = snapshot.anchor_after(selection.end);
- new_selections
- .push((selection.map(|_| anchor), region.pair.end.len()));
- continue;
- }
- }
-
- let always_treat_brackets_as_autoclosed = snapshot
- .language_settings_at(selection.start, cx)
- .always_treat_brackets_as_autoclosed;
- if always_treat_brackets_as_autoclosed
- && is_bracket_pair_end
- && snapshot.contains_str_at(selection.end, text.as_ref())
- {
- // Otherwise, when `always_treat_brackets_as_autoclosed` is set to `true
- // and the inserted text is a closing bracket and the selection is followed
- // by the closing bracket then move the selection past the closing bracket.
- let anchor = snapshot.anchor_after(selection.end);
- new_selections.push((selection.map(|_| anchor), text.len()));
- continue;
- }
- }
- // If an opening bracket is 1 character long and is typed while
- // text is selected, then surround that text with the bracket pair.
- else if auto_surround
- && bracket_pair.surround
- && is_bracket_pair_start
- && bracket_pair.start.chars().count() == 1
- {
- edits.push((selection.start..selection.start, text.clone()));
- edits.push((
- selection.end..selection.end,
- bracket_pair.end.as_str().into(),
- ));
- bracket_inserted = true;
- 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 self.auto_replace_emoji_shortcode
- && selection.is_empty()
- && text.as_ref().ends_with(':')
- && let Some(possible_emoji_short_code) =
- Self::find_possible_emoji_shortcode_at_position(&snapshot, selection.start)
- && !possible_emoji_short_code.is_empty()
- && let Some(emoji) = emojis::get_by_shortcode(&possible_emoji_short_code)
- {
- let emoji_shortcode_start = Point::new(
- selection.start.row,
- selection.start.column - possible_emoji_short_code.len() as u32 - 1,
- );
-
- // Remove shortcode from buffer
- edits.push((
- emoji_shortcode_start..selection.start,
- "".to_string().into(),
- ));
- new_selections.push((
- Selection {
- id: selection.id,
- start: snapshot.anchor_after(emoji_shortcode_start),
- end: snapshot.anchor_before(selection.start),
- reversed: selection.reversed,
- goal: selection.goal,
- },
- 0,
- ));
-
- // Insert emoji
- let selection_start_anchor = snapshot.anchor_after(selection.start);
- new_selections.push((selection.map(|_| selection_start_anchor), 0));
- edits.push((selection.start..selection.end, emoji.to_string().into()));
-
- continue;
- }
-
- let next_is_adjacent = regions
- .peek()
- .is_some_and(|(next, _)| selection.end == next.start);
-
- // 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 = if in_adjacent_group || next_is_adjacent {
- // After edits the right bias would shift those anchor to the next visible fragment
- // but we want to resolve to the previous one
- snapshot.anchor_before(selection.end)
- } else {
- snapshot.anchor_after(selection.end)
- };
-
- if !self.linked_edit_ranges.is_empty() {
- let start_anchor = snapshot.anchor_before(selection.start);
- let classifier = snapshot
- .char_classifier_at(start_anchor)
- .scope_context(Some(CharScopeContext::LinkedEdit));
-
- if let Some((_, anchor_range)) =
- snapshot.anchor_range_to_buffer_anchor_range(start_anchor..anchor)
- {
- let is_word_char = text
- .chars()
- .next()
- .is_none_or(|char| classifier.is_word(char));
-
- let is_dot = text.as_ref() == ".";
- let should_apply_linked_edit = is_word_char || is_dot;
-
- if should_apply_linked_edit {
- linked_edits.push(&self, anchor_range, text.clone(), cx);
- } else {
- clear_linked_edit_ranges = true;
- }
- }
- }
-
- new_selections.push((selection.map(|_| anchor), 0));
- edits.push((selection.start..selection.end, text.clone()));
-
- has_adjacent_edits |= next_is_adjacent;
- in_adjacent_group = next_is_adjacent;
- }
-
- if all_selections_read_only {
- return;
- }
-
- drop(regions);
- drop(snapshot);
-
- self.transact(window, cx, |this, window, cx| {
- if clear_linked_edit_ranges {
- this.linked_edit_ranges.clear();
- }
- let initial_buffer_versions =
- jsx_tag_auto_close::construct_initial_buffer_versions_map(this, &edits, cx);
-
- this.buffer.update(cx, |buffer, cx| {
- if has_adjacent_edits {
- buffer.edit_non_coalesce(edits, this.autoindent_mode.clone(), cx);
- } else {
- buffer.edit(edits, this.autoindent_mode.clone(), cx);
- }
- });
- linked_edits.apply(cx);
- let new_anchor_selections = new_selections.iter().map(|e| &e.0);
- let new_selection_deltas = new_selections.iter().map(|e| e.1);
- let map = this.display_map.update(cx, |map, cx| map.snapshot(cx));
- let new_selections = resolve_selections_wrapping_blocks::<MultiBufferOffset, _>(
- new_anchor_selections,
- &map,
- )
- .zip(new_selection_deltas)
- .map(|(selection, delta)| Selection {
- id: selection.id,
- start: selection.start + delta,
- end: selection.end + delta,
- reversed: selection.reversed,
- goal: SelectionGoal::None,
- })
- .collect::<Vec<_>>();
-
- let mut i = 0;
- for (position, delta, selection_id, pair) in new_autoclose_regions {
- let position = position.to_offset(map.buffer_snapshot()) + delta;
- let start = map.buffer_snapshot().anchor_before(position);
- let end = map.buffer_snapshot().anchor_after(position);
- while let Some(existing_state) = this.autoclose_regions.get(i) {
- match existing_state
- .range
- .start
- .cmp(&start, map.buffer_snapshot())
- {
- Ordering::Less => i += 1,
- Ordering::Greater => break,
- Ordering::Equal => {
- match end.cmp(&existing_state.range.end, map.buffer_snapshot()) {
- Ordering::Less => i += 1,
- Ordering::Equal => break,
- Ordering::Greater => break,
- }
- }
- }
- }
- this.autoclose_regions.insert(
- i,
- AutocloseRegion {
- selection_id,
- range: start..end,
- pair,
- },
- );
- }
-
- let had_active_edit_prediction = this.has_active_edit_prediction();
- this.change_selections(
- SelectionEffects::scroll(Autoscroll::fit()).completions(false),
- window,
- cx,
- |s| s.select(new_selections),
- );
-
- if !bracket_inserted
- && let Some(on_type_format_task) =
- this.trigger_on_type_formatting(text.to_string(), window, cx)
- {
- on_type_format_task.detach_and_log_err(cx);
- }
-
- let editor_settings = EditorSettings::get_global(cx);
- if bracket_inserted
- && (editor_settings.auto_signature_help
- || editor_settings.show_signature_help_after_edits)
- {
- this.show_signature_help(&ShowSignatureHelp, window, cx);
- }
-
- let trigger_in_words =
- this.show_edit_predictions_in_menu() || !had_active_edit_prediction;
- if this.hard_wrap.is_some() {
- let latest: Range<Point> = this.selections.newest(&map).range();
- if latest.is_empty()
- && this
- .buffer()
- .read(cx)
- .snapshot(cx)
- .line_len(MultiBufferRow(latest.start.row))
- == latest.start.column
- {
- this.rewrap(
- RewrapOptions {
- override_language_settings: true,
- preserve_existing_whitespace: true,
- line_length: None,
- },
- cx,
- )
- }
- }
- this.trigger_completion_on_input(&text, trigger_in_words, window, cx);
- refresh_linked_ranges(this, window, cx);
- this.refresh_edit_prediction(true, false, window, cx);
- jsx_tag_auto_close::handle_from(this, initial_buffer_versions, window, cx);
- });
- }
-
- fn find_possible_emoji_shortcode_at_position(
- snapshot: &MultiBufferSnapshot,
- position: Point,
- ) -> Option<String> {
- let mut chars = Vec::new();
- let mut found_colon = false;
- for char in snapshot.reversed_chars_at(position).take(100) {
- // Found a possible emoji shortcode in the middle of the buffer
- if found_colon {
- if char.is_whitespace() {
- chars.reverse();
- return Some(chars.iter().collect());
- }
- // If the previous character is not a whitespace, we are in the middle of a word
- // and we only want to complete the shortcode if the word is made up of other emojis
- let mut containing_word = String::new();
- for ch in snapshot
- .reversed_chars_at(position)
- .skip(chars.len() + 1)
- .take(100)
- {
- if ch.is_whitespace() {
- break;
- }
- containing_word.push(ch);
- }
- let containing_word = containing_word.chars().rev().collect::<String>();
- if util::word_consists_of_emojis(containing_word.as_str()) {
- chars.reverse();
- return Some(chars.iter().collect());
- }
- }
-
- if char.is_whitespace() || !char.is_ascii() {
- return None;
- }
- if char == ':' {
- found_colon = true;
- } else {
- chars.push(char);
- }
- }
- // Found a possible emoji shortcode at the beginning of the buffer
- chars.reverse();
- Some(chars.iter().collect())
- }
-
- pub fn newline(&mut self, _: &Newline, window: &mut Window, cx: &mut Context<Self>) {
- if self.read_only(cx) {
- return;
- }
-
- self.transact(window, cx, |this, window, cx| {
- let (edits_with_flags, selection_info): (Vec<_>, Vec<_>) = {
- let selections = this
- .selections
- .all::<MultiBufferOffset>(&this.display_snapshot(cx));
- let multi_buffer = this.buffer.read(cx);
- let buffer = multi_buffer.snapshot(cx);
- selections
- .iter()
- .map(|selection| {
- let start_point = selection.start.to_point(&buffer);
- let mut existing_indent =
- buffer.indent_size_for_line(MultiBufferRow(start_point.row));
- let full_indent_len = existing_indent.len;
- existing_indent.len = cmp::min(existing_indent.len, start_point.column);
- let mut start = selection.start;
- let end = selection.end;
- let selection_is_empty = start == end;
- let language_scope = buffer.language_scope_at(start);
- let (delimiter, newline_config) = if let Some(language) = &language_scope {
- let needs_extra_newline = NewlineConfig::insert_extra_newline_brackets(
- &buffer,
- start..end,
- language,
- )
- || NewlineConfig::insert_extra_newline_tree_sitter(
- &buffer,
- start..end,
- );
-
- let mut newline_config = NewlineConfig::Newline {
- additional_indent: IndentSize::spaces(0),
- extra_line_additional_indent: if needs_extra_newline {
- Some(IndentSize::spaces(0))
- } else {
- None
- },
- prevent_auto_indent: false,
- };
-
- let comment_delimiter = maybe!({
- if !selection_is_empty {
- return None;
- }
-
- if !multi_buffer.language_settings(cx).extend_comment_on_newline {
- return None;
- }
-
- return comment_delimiter_for_newline(
- &start_point,
- &buffer,
- language,
- );
- });
-
- let doc_delimiter = maybe!({
- if !selection_is_empty {
- return None;
- }
-
- if !multi_buffer.language_settings(cx).extend_comment_on_newline {
- return None;
- }
-
- return documentation_delimiter_for_newline(
- &start_point,
- &buffer,
- language,
- &mut newline_config,
- );
- });
-
- let list_delimiter = maybe!({
- if !selection_is_empty {
- return None;
- }
-
- if !multi_buffer.language_settings(cx).extend_list_on_newline {
- return None;
- }
-
- return list_delimiter_for_newline(
- &start_point,
- &buffer,
- language,
- &mut newline_config,
- );
- });
-
- (
- comment_delimiter.or(doc_delimiter).or(list_delimiter),
- newline_config,
- )
- } else {
- (
- None,
- NewlineConfig::Newline {
- additional_indent: IndentSize::spaces(0),
- extra_line_additional_indent: None,
- prevent_auto_indent: false,
- },
- )
- };
-
- let (edit_start, new_text, prevent_auto_indent) = match &newline_config {
- NewlineConfig::ClearCurrentLine => {
- let row_start =
- buffer.point_to_offset(Point::new(start_point.row, 0));
- (row_start, String::new(), false)
- }
- NewlineConfig::UnindentCurrentLine { continuation } => {
- let row_start =
- buffer.point_to_offset(Point::new(start_point.row, 0));
- let tab_size = buffer.language_settings_at(start, cx).tab_size;
- let tab_size_indent = IndentSize::spaces(tab_size.get());
- let reduced_indent =
- existing_indent.with_delta(Ordering::Less, tab_size_indent);
- let mut new_text = String::new();
- new_text.extend(reduced_indent.chars());
- new_text.push_str(continuation);
- (row_start, new_text, true)
- }
- NewlineConfig::Newline {
- additional_indent,
- extra_line_additional_indent,
- prevent_auto_indent,
- } => {
- let auto_indent_mode =
- buffer.language_settings_at(start, cx).auto_indent;
- let preserve_indent =
- auto_indent_mode != language::AutoIndentMode::None;
- let apply_syntax_indent =
- auto_indent_mode == language::AutoIndentMode::SyntaxAware;
- let capacity_for_delimiter =
- delimiter.as_deref().map(str::len).unwrap_or_default();
- let existing_indent_len = if preserve_indent {
- existing_indent.len as usize
- } else {
- 0
- };
- let extra_line_len = extra_line_additional_indent
- .map(|i| 1 + existing_indent_len + i.len as usize)
- .unwrap_or(0);
- let mut new_text = String::with_capacity(
- 1 + capacity_for_delimiter
- + existing_indent_len
- + additional_indent.len as usize
- + extra_line_len,
- );
- new_text.push('\n');
- if preserve_indent {
- new_text.extend(existing_indent.chars());
- }
- new_text.extend(additional_indent.chars());
- if let Some(delimiter) = &delimiter {
- new_text.push_str(delimiter);
- }
- if let Some(extra_indent) = extra_line_additional_indent {
- new_text.push('\n');
- if preserve_indent {
- new_text.extend(existing_indent.chars());
- }
- new_text.extend(extra_indent.chars());
- }
- // Extend the edit to the beginning of the line
- // to clear auto-indent whitespace that would
- // otherwise remain as trailing whitespace. This
- // applies to blank lines and lines where only
- // indentation remains before the cursor.
- if selection_is_empty
- && preserve_indent
- && full_indent_len > 0
- && start_point.column == full_indent_len
- {
- start = buffer.point_to_offset(Point::new(start_point.row, 0));
- }
-
- (
- start,
- new_text,
- *prevent_auto_indent || !apply_syntax_indent,
- )
- }
- };
-
- let anchor = buffer.anchor_after(end);
- let new_selection = selection.map(|_| anchor);
- (
- ((edit_start..end, new_text), prevent_auto_indent),
- (newline_config.has_extra_line(), new_selection),
- )
- })
- .unzip()
- };
-
- let mut auto_indent_edits = Vec::new();
- let mut edits = Vec::new();
- for (edit, prevent_auto_indent) in edits_with_flags {
- if prevent_auto_indent {
- edits.push(edit);
- } else {
- auto_indent_edits.push(edit);
- }
- }
- if !edits.is_empty() {
- this.edit(edits, cx);
- }
- if !auto_indent_edits.is_empty() {
- this.edit_with_autoindent(auto_indent_edits, cx);
- }
-
- let buffer = this.buffer.read(cx).snapshot(cx);
- let new_selections = selection_info
- .into_iter()
- .map(|(extra_newline_inserted, new_selection)| {
- let mut cursor = new_selection.end.to_point(&buffer);
- if extra_newline_inserted {
- cursor.row -= 1;
- cursor.column = buffer.line_len(MultiBufferRow(cursor.row));
- }
- new_selection.map(|_| cursor)
- })
- .collect();
-
- this.change_selections(Default::default(), window, cx, |s| s.select(new_selections));
- this.refresh_edit_prediction(true, false, window, cx);
- if let Some(task) = this.trigger_on_type_formatting("\n".to_owned(), window, cx) {
- task.detach_and_log_err(cx);
- }
- });
- }
-
- pub fn newline_above(&mut self, _: &NewlineAbove, window: &mut Window, cx: &mut Context<Self>) {
- if self.read_only(cx) {
- return;
- }
-
- let buffer = self.buffer.read(cx);
- let snapshot = buffer.snapshot(cx);
-
- let mut edits = Vec::new();
- let mut rows = Vec::new();
-
- for (rows_inserted, selection) in self
- .selections
- .all_adjusted(&self.display_snapshot(cx))
- .into_iter()
- .enumerate()
- {
- let cursor = selection.head();
- let row = cursor.row;
-
- let start_of_line = snapshot.clip_point(Point::new(row, 0), Bias::Left);
-
- let newline = "\n".to_string();
- edits.push((start_of_line..start_of_line, newline));
-
- rows.push(row + rows_inserted as u32);
- }
-
- self.transact(window, cx, |editor, window, cx| {
- editor.edit(edits, cx);
-
- editor.change_selections(Default::default(), window, cx, |s| {
- let mut index = 0;
- s.move_cursors_with(&mut |map, _, _| {
- let row = rows[index];
- index += 1;
-
- let point = Point::new(row, 0);
- let boundary = map.next_line_boundary(point).1;
- let clipped = map.clip_point(boundary, Bias::Left);
-
- (clipped, SelectionGoal::None)
- });
- });
-
- let mut indent_edits = Vec::new();
- let multibuffer_snapshot = editor.buffer.read(cx).snapshot(cx);
- for row in rows {
- let indents = multibuffer_snapshot.suggested_indents(row..row + 1, cx);
- for (row, indent) in indents {
- if indent.len == 0 {
- continue;
- }
-
- let text = match indent.kind {
- IndentKind::Space => " ".repeat(indent.len as usize),
- IndentKind::Tab => "\t".repeat(indent.len as usize),
- };
- let point = Point::new(row.0, 0);
- indent_edits.push((point..point, text));
- }
- }
- editor.edit(indent_edits, cx);
- if let Some(format) = editor.trigger_on_type_formatting("\n".to_owned(), window, cx) {
- format.detach_and_log_err(cx);
- }
- });
- }
-
- pub fn newline_below(&mut self, _: &NewlineBelow, window: &mut Window, cx: &mut Context<Self>) {
- if self.read_only(cx) {
- return;
- }
-
- let mut buffer_edits: HashMap<EntityId, (Entity<Buffer>, Vec<Point>)> = HashMap::default();
- let mut rows = Vec::new();
- let mut rows_inserted = 0;
-
- for selection in self.selections.all_adjusted(&self.display_snapshot(cx)) {
- let cursor = selection.head();
- let row = cursor.row;
-
- let point = Point::new(row, 0);
- let Some((buffer_handle, buffer_point)) =
- self.buffer.read(cx).point_to_buffer_point(point, cx)
- else {
- continue;
- };
-
- buffer_edits
- .entry(buffer_handle.entity_id())
- .or_insert_with(|| (buffer_handle, Vec::new()))
- .1
- .push(buffer_point);
-
- rows_inserted += 1;
- rows.push(row + rows_inserted);
- }
-
- self.transact(window, cx, |editor, window, cx| {
- for (_, (buffer_handle, points)) in &buffer_edits {
- buffer_handle.update(cx, |buffer, cx| {
- let edits: Vec<_> = points
- .iter()
- .map(|point| {
- let target = Point::new(point.row + 1, 0);
- let start_of_line = buffer.point_to_offset(target).min(buffer.len());
- (start_of_line..start_of_line, "\n")
- })
- .collect();
- buffer.edit(edits, None, cx);
- });
- }
-
- editor.change_selections(Default::default(), window, cx, |s| {
- let mut index = 0;
- s.move_cursors_with(&mut |map, _, _| {
- let row = rows[index];
- index += 1;
-
- let point = Point::new(row, 0);
- let boundary = map.next_line_boundary(point).1;
- let clipped = map.clip_point(boundary, Bias::Left);
-
- (clipped, SelectionGoal::None)
- });
- });
-
- let mut indent_edits = Vec::new();
- let multibuffer_snapshot = editor.buffer.read(cx).snapshot(cx);
- for row in rows {
- let indents = multibuffer_snapshot.suggested_indents(row..row + 1, cx);
- for (row, indent) in indents {
- if indent.len == 0 {
- continue;
- }
-
- let text = match indent.kind {
- IndentKind::Space => " ".repeat(indent.len as usize),
- IndentKind::Tab => "\t".repeat(indent.len as usize),
- };
- let point = Point::new(row.0, 0);
- indent_edits.push((point..point, text));
- }
- }
- editor.edit(indent_edits, cx);
- if let Some(format) = editor.trigger_on_type_formatting("\n".to_owned(), window, cx) {
- format.detach_and_log_err(cx);
- }
- });
- }
-
- pub fn insert(&mut self, text: &str, window: &mut Window, cx: &mut Context<Self>) {
- let autoindent = text.is_empty().not().then(|| AutoindentMode::Block {
- original_indent_columns: Vec::new(),
- });
- self.replace_selections(text, autoindent, window, cx, false);
- }
-
- /// Replaces the editor's selections with the provided `text`, applying the
- /// given `autoindent_mode` (`None` will skip autoindentation).
- ///
- /// Early returns if the editor is in read-only mode, without applying any
- /// edits.
- fn replace_selections(
- &mut self,
- text: &str,
- autoindent_mode: Option<AutoindentMode>,
- window: &mut Window,
- cx: &mut Context<Self>,
- apply_linked_edits: bool,
- ) {
- if self.read_only(cx) {
- return;
- }
-
- let text: Arc<str> = text.into();
- self.transact(window, cx, |this, window, cx| {
- let old_selections = this.selections.all_adjusted(&this.display_snapshot(cx));
- let linked_edits = if apply_linked_edits {
- this.linked_edits_for_selections(text.clone(), cx)
- } else {
- LinkedEdits::new()
- };
-
- let selection_anchors = this.buffer.update(cx, |buffer, cx| {
- let anchors = {
- let snapshot = buffer.read(cx);
- old_selections
- .iter()
- .map(|s| {
- let anchor = snapshot.anchor_after(s.head());
- s.map(|_| anchor)
- })
- .collect::<Vec<_>>()
- };
- buffer.edit(
- old_selections
- .iter()
- .map(|s| (s.start..s.end, text.clone())),
- autoindent_mode,
- cx,
- );
- anchors
- });
-
- linked_edits.apply(cx);
-
- this.change_selections(Default::default(), window, cx, |s| {
- s.select_anchors(selection_anchors);
- });
-
- if apply_linked_edits {
- refresh_linked_ranges(this, window, cx);
- }
-
- cx.notify();
- });
- }
-
- /// Collects linked edits for the current selections, pairing each linked
- /// range with `text`.
- pub fn linked_edits_for_selections(&self, text: Arc<str>, cx: &App) -> LinkedEdits {
- let multibuffer_snapshot = self.buffer().read(cx).snapshot(cx);
- let mut linked_edits = LinkedEdits::new();
- if !self.linked_edit_ranges.is_empty() {
- for selection in self.selections.disjoint_anchors() {
- let Some((_, range)) =
- multibuffer_snapshot.anchor_range_to_buffer_anchor_range(selection.range())
- else {
- continue;
- };
- linked_edits.push(self, range, text.clone(), cx);
- }
- }
- linked_edits
- }
-
- /// Deletes the content covered by the current selections and applies
- /// linked edits.
- pub fn delete_selections_with_linked_edits(
- &mut self,
- window: &mut Window,
- cx: &mut Context<Self>,
- ) {
- self.replace_selections("", None, window, cx, true);
- }
-
- #[cfg(any(test, feature = "test-support"))]
- pub fn set_linked_edit_ranges_for_testing(
- &mut self,
- ranges: Vec<(Range<Point>, Vec<Range<Point>>)>,
- cx: &mut Context<Self>,
- ) -> Option<()> {
- let Some((buffer, _)) = self
- .buffer
- .read(cx)
- .text_anchor_for_position(self.selections.newest_anchor().start, cx)
- else {
- return None;
- };
- let buffer = buffer.read(cx);
- let buffer_id = buffer.remote_id();
- let mut linked_ranges = Vec::with_capacity(ranges.len());
- for (base_range, linked_ranges_points) in ranges {
- let base_anchor =
- buffer.anchor_before(base_range.start)..buffer.anchor_after(base_range.end);
- let linked_anchors = linked_ranges_points
- .into_iter()
- .map(|range| buffer.anchor_before(range.start)..buffer.anchor_after(range.end))
- .collect();
- linked_ranges.push((base_anchor, linked_anchors));
- }
- let mut map = HashMap::default();
- map.insert(buffer_id, linked_ranges);
- self.linked_edit_ranges = linked_editing_ranges::LinkedEditingRanges(map);
- Some(())
- }
-
- /// 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, window: &mut Window, cx: &mut Context<Self>) {
- let selections = self
- .selections
- .all::<MultiBufferOffset>(&self.display_snapshot(cx));
- let buffer = self.buffer.read(cx).read(cx);
- let new_selections = self
- .selections_with_autoclose_regions(selections, &buffer)
- .map(|(mut selection, region)| {
- if !selection.is_empty() {
- return selection;
- }
-
- if let Some(region) = region {
- let mut range = region.range.to_offset(&buffer);
- if selection.start == range.start && range.start.0 >= region.pair.start.len() {
- range.start -= region.pair.start.len();
- if buffer.contains_str_at(range.start, ®ion.pair.start)
- && buffer.contains_str_at(range.end, ®ion.pair.end)
- {
- range.end += region.pair.end.len();
- selection.start = range.start;
- selection.end = range.end;
-
- return selection;
- }
- }
- }
-
- let always_treat_brackets_as_autoclosed = buffer
- .language_settings_at(selection.start, cx)
- .always_treat_brackets_as_autoclosed;
-
- if !always_treat_brackets_as_autoclosed {
- return selection;
- }
-
- if let Some(scope) = buffer.language_scope_at(selection.start) {
- for (pair, enabled) in scope.brackets() {
- if !enabled || !pair.close {
- continue;
- }
-
- if buffer.contains_str_at(selection.start, &pair.end) {
- let pair_start_len = pair.start.len();
- if buffer.contains_str_at(
- selection.start.saturating_sub_usize(pair_start_len),
- &pair.start,
- ) {
- selection.start -= pair_start_len;
- selection.end += pair.end.len();
-
- return selection;
- }
- }
- }
- }
-
- selection
- })
- .collect();
-
- drop(buffer);
- self.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
- selections.select(new_selections)
- });
- }
-
- /// 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<Item = Selection<D>>,
- buffer: &'a MultiBufferSnapshot,
- ) -> impl Iterator<Item = (Selection<D>, Option<&'a AutocloseRegion>)> {
- let mut i = 0;
- let mut regions = 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) = regions.get(i) {
- if pair_state.range.end.to_offset(buffer) < range.start {
- regions = ®ions[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;
- }
- }
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) -> bool {
+ let mut dismissed = false;
- (selection, enclosing)
- })
- }
+ dismissed |= self.take_rename(false, window, cx).is_some();
+ dismissed |= self.hide_blame_popover(true, cx);
+ dismissed |= hide_hover(self, cx);
+ dismissed |= self.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
+ dismissed |= self.hide_context_menu(window, cx).is_some();
+ dismissed |= self.mouse_context_menu.take().is_some();
+ dismissed |= is_user_requested
+ && self.discard_edit_prediction(EditPredictionDiscardReason::Rejected, cx);
+ dismissed |= self.snippet_stack.pop().is_some();
+ if self.diff_review_drag_state.is_some() {
+ self.cancel_diff_review_drag(cx);
+ dismissed = true;
+ }
+ if !self.diff_review_overlays.is_empty() {
+ self.dismiss_all_diff_review_overlays(cx);
+ dismissed = true;
+ }
- /// Remove any autoclose regions that no longer contain their selection or have invalid anchors in ranges.
- fn invalidate_autoclose_regions(
- &mut self,
- mut selections: &[Selection<Anchor>],
- buffer: &MultiBufferSnapshot,
- ) {
- self.autoclose_regions.retain(|state| {
- if !state.range.start.is_valid(buffer) || !state.range.end.is_valid(buffer) {
- return false;
- }
+ if self.mode.is_full() && self.has_active_diagnostic_group() {
+ self.dismiss_diagnostics(cx);
+ dismissed = true;
+ }
- 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;
- }
- }
- false
- });
+ dismissed
}
fn open_transaction_for_hidden_buffers(
@@ -1 +1,710 @@
pub mod blame;
+
+use super::*;
+
+impl Editor {
+ pub fn diff_hunks_in_ranges<'a>(
+ &'a self,
+ ranges: &'a [Range<Anchor>],
+ buffer: &'a MultiBufferSnapshot,
+ ) -> impl 'a + Iterator<Item = MultiBufferDiffHunk> {
+ ranges.iter().flat_map(move |range| {
+ let end_excerpt = buffer.excerpt_containing(range.end..range.end);
+ let range = range.to_point(buffer);
+ let mut peek_end = range.end;
+ if range.end.row < buffer.max_row().0 {
+ peek_end = Point::new(range.end.row + 1, 0);
+ }
+ buffer
+ .diff_hunks_in_range(range.start..peek_end)
+ .filter(move |hunk| {
+ if let Some((_, excerpt_range)) = &end_excerpt
+ && let Some(end_anchor) =
+ buffer.anchor_in_excerpt(excerpt_range.context.end)
+ && let Some(hunk_end_anchor) =
+ buffer.anchor_in_excerpt(hunk.excerpt_range.context.end)
+ && hunk_end_anchor.cmp(&end_anchor, buffer).is_gt()
+ {
+ false
+ } else {
+ true
+ }
+ })
+ })
+ }
+
+ pub fn set_render_diff_hunk_controls(
+ &mut self,
+ render_diff_hunk_controls: RenderDiffHunkControlsFn,
+ cx: &mut Context<Self>,
+ ) {
+ self.render_diff_hunk_controls = render_diff_hunk_controls;
+ cx.notify();
+ }
+
+ pub fn working_directory(&self, cx: &App) -> Option<PathBuf> {
+ if let Some(buffer) = self.buffer().read(cx).as_singleton() {
+ if let Some(file) = buffer.read(cx).file().and_then(|f| f.as_local())
+ && let Some(dir) = file.abs_path(cx).parent()
+ {
+ return Some(dir.to_owned());
+ }
+ }
+
+ None
+ }
+
+ pub fn target_file_abs_path(&self, cx: &mut Context<Self>) -> Option<PathBuf> {
+ self.active_buffer(cx).and_then(|buffer| {
+ let buffer = buffer.read(cx);
+ if let Some(project_path) = buffer.project_path(cx) {
+ let project = self.project()?.read(cx);
+ project.absolute_path(&project_path, cx)
+ } else {
+ buffer
+ .file()
+ .and_then(|file| file.as_local().map(|file| file.abs_path(cx)))
+ }
+ })
+ }
+
+ /// Returns the project path for the editor's buffer, if any buffer is
+ /// opened in the editor.
+ pub fn project_path(&self, cx: &App) -> Option<ProjectPath> {
+ if let Some(buffer) = self.buffer.read(cx).as_singleton() {
+ buffer.read(cx).project_path(cx)
+ } else {
+ None
+ }
+ }
+
+ pub fn git_blame_inline_enabled(&self) -> bool {
+ self.git_blame_inline_enabled
+ }
+
+ pub fn selection_menu_enabled(&self, cx: &App) -> bool {
+ self.show_selection_menu
+ .unwrap_or_else(|| EditorSettings::get_global(cx).toolbar.selections_menu)
+ }
+
+ pub fn toggle_selection_menu(
+ &mut self,
+ _: &ToggleSelectionMenu,
+ _: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ self.show_selection_menu = self
+ .show_selection_menu
+ .map(|show_selections_menu| !show_selections_menu)
+ .or_else(|| Some(!EditorSettings::get_global(cx).toolbar.selections_menu));
+
+ cx.notify();
+ }
+
+ pub fn blame(&self) -> Option<&Entity<GitBlame>> {
+ self.blame.as_ref()
+ }
+
+ pub fn show_git_blame_gutter(&self) -> bool {
+ self.show_git_blame_gutter
+ }
+
+ pub fn expand_selected_diff_hunks(&mut self, cx: &mut Context<Self>) {
+ let ranges: Vec<_> = self
+ .selections
+ .disjoint_anchors()
+ .iter()
+ .map(|s| s.range())
+ .collect();
+ self.buffer
+ .update(cx, |buffer, cx| buffer.expand_diff_hunks(ranges, cx))
+ }
+
+ pub fn copy_file_name_without_extension(
+ &mut self,
+ _: &CopyFileNameWithoutExtension,
+ _: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ if let Some(file_stem) = self.active_buffer(cx).and_then(|buffer| {
+ let file = buffer.read(cx).file()?;
+ file.path().file_stem()
+ }) {
+ cx.write_to_clipboard(ClipboardItem::new_string(file_stem.to_string()));
+ }
+ }
+
+ pub fn copy_file_name(&mut self, _: &CopyFileName, _: &mut Window, cx: &mut Context<Self>) {
+ if let Some(file_name) = self.active_buffer(cx).and_then(|buffer| {
+ let file = buffer.read(cx).file()?;
+ Some(file.file_name(cx))
+ }) {
+ cx.write_to_clipboard(ClipboardItem::new_string(file_name.to_string()));
+ }
+ }
+
+ pub fn toggle_git_blame(
+ &mut self,
+ _: &::git::Blame,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ self.show_git_blame_gutter = !self.show_git_blame_gutter;
+
+ if self.show_git_blame_gutter && !self.has_blame_entries(cx) {
+ self.start_git_blame(true, window, cx);
+ }
+
+ cx.notify();
+ }
+
+ pub fn toggle_git_blame_inline(
+ &mut self,
+ _: &ToggleGitBlameInline,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ self.toggle_git_blame_inline_internal(true, window, cx);
+ cx.notify();
+ }
+
+ pub(super) fn toggle_staged_selected_diff_hunks(
+ &mut self,
+ _: &::git::ToggleStaged,
+ _: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ let snapshot = self.buffer.read(cx).snapshot(cx);
+ let ranges: Vec<_> = self
+ .selections
+ .disjoint_anchors()
+ .iter()
+ .map(|s| s.range())
+ .collect();
+ let stage = self.has_stageable_diff_hunks_in_ranges(&ranges, &snapshot);
+ self.stage_or_unstage_diff_hunks(stage, ranges, cx);
+ }
+
+ pub(super) fn stage_and_next(
+ &mut self,
+ _: &::git::StageAndNext,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ self.do_stage_or_unstage_and_next(true, window, cx);
+ }
+
+ pub(super) fn unstage_and_next(
+ &mut self,
+ _: &::git::UnstageAndNext,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ self.do_stage_or_unstage_and_next(false, window, cx);
+ }
+
+ pub(super) fn stage_or_unstage_diff_hunks(
+ &mut self,
+ stage: bool,
+ ranges: Vec<Range<Anchor>>,
+ cx: &mut Context<Self>,
+ ) {
+ if self.delegate_stage_and_restore {
+ let snapshot = self.buffer.read(cx).snapshot(cx);
+ let hunks: Vec<_> = self.diff_hunks_in_ranges(&ranges, &snapshot).collect();
+ if !hunks.is_empty() {
+ cx.emit(EditorEvent::StageOrUnstageRequested { stage, hunks });
+ }
+ return;
+ }
+ let task = self.save_buffers_for_ranges_if_needed(&ranges, cx);
+ cx.spawn(async move |this, cx| {
+ task.await?;
+ this.update(cx, |this, cx| {
+ let snapshot = this.buffer.read(cx).snapshot(cx);
+ let chunk_by = this
+ .diff_hunks_in_ranges(&ranges, &snapshot)
+ .chunk_by(|hunk| hunk.buffer_id);
+ for (buffer_id, hunks) in &chunk_by {
+ this.do_stage_or_unstage(stage, buffer_id, hunks, cx);
+ }
+ })
+ })
+ .detach_and_log_err(cx);
+ }
+
+ pub(super) fn do_stage_or_unstage(
+ &self,
+ stage: bool,
+ buffer_id: BufferId,
+ hunks: impl Iterator<Item = MultiBufferDiffHunk>,
+ cx: &mut App,
+ ) -> Option<()> {
+ let project = self.project()?;
+ let buffer = project.read(cx).buffer_for_id(buffer_id, cx)?;
+ let diff = self.buffer.read(cx).diff_for(buffer_id)?;
+ let buffer_snapshot = buffer.read(cx).snapshot();
+ let file_exists = buffer_snapshot
+ .file()
+ .is_some_and(|file| file.disk_state().exists());
+ diff.update(cx, |diff, cx| {
+ diff.stage_or_unstage_hunks(
+ stage,
+ &hunks
+ .map(|hunk| buffer_diff::DiffHunk {
+ buffer_range: hunk.buffer_range,
+ // We don't need to pass in word diffs here because they're only used for rendering and
+ // this function changes internal state
+ base_word_diffs: Vec::default(),
+ buffer_word_diffs: Vec::default(),
+ diff_base_byte_range: hunk.diff_base_byte_range.start.0
+ ..hunk.diff_base_byte_range.end.0,
+ secondary_status: hunk.status.secondary,
+ range: Point::zero()..Point::zero(), // unused
+ })
+ .collect::<Vec<_>>(),
+ &buffer_snapshot,
+ file_exists,
+ cx,
+ )
+ });
+ None
+ }
+
+ pub(super) fn clear_expanded_diff_hunks(&mut self, cx: &mut Context<Self>) -> bool {
+ self.buffer.update(cx, |buffer, cx| {
+ let ranges = vec![Anchor::Min..Anchor::Max];
+ if !buffer.all_diff_hunks_expanded()
+ && buffer.has_expanded_diff_hunks_in_ranges(&ranges, cx)
+ {
+ buffer.collapse_diff_hunks(ranges, cx);
+ true
+ } else {
+ false
+ }
+ })
+ }
+
+ pub(super) fn has_any_expanded_diff_hunks(&self, cx: &App) -> bool {
+ if self.buffer.read(cx).all_diff_hunks_expanded() {
+ return true;
+ }
+ let ranges = vec![Anchor::Min..Anchor::Max];
+ self.buffer
+ .read(cx)
+ .has_expanded_diff_hunks_in_ranges(&ranges, cx)
+ }
+
+ pub(super) fn toggle_diff_hunks_in_ranges(
+ &mut self,
+ ranges: Vec<Range<Anchor>>,
+ cx: &mut Context<Editor>,
+ ) {
+ self.buffer.update(cx, |buffer, cx| {
+ let expand = !buffer.has_expanded_diff_hunks_in_ranges(&ranges, cx);
+ buffer.expand_or_collapse_diff_hunks(ranges, expand, cx);
+ })
+ }
+
+ pub(super) fn toggle_single_diff_hunk(&mut self, range: Range<Anchor>, cx: &mut Context<Self>) {
+ self.buffer.update(cx, |buffer, cx| {
+ buffer.toggle_single_diff_hunk(range, cx);
+ })
+ }
+
+ pub(super) fn apply_all_diff_hunks(
+ &mut self,
+ _: &ApplyAllDiffHunks,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ if self.read_only(cx) {
+ return;
+ }
+
+ let buffers = self.buffer.read(cx).all_buffers();
+ for branch_buffer in buffers {
+ branch_buffer.update(cx, |branch_buffer, cx| {
+ branch_buffer.merge_into_base(Vec::new(), cx);
+ });
+ }
+
+ if let Some(project) = self.project.clone() {
+ self.save(
+ SaveOptions {
+ format: true,
+ force_format: false,
+ autosave: false,
+ },
+ project,
+ window,
+ cx,
+ )
+ .detach_and_log_err(cx);
+ }
+ }
+
+ pub(super) fn apply_selected_diff_hunks(
+ &mut self,
+ _: &ApplyDiffHunk,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ if self.read_only(cx) {
+ return;
+ }
+ let snapshot = self.snapshot(window, cx);
+ let hunks = snapshot.hunks_for_ranges(
+ self.selections
+ .all(&snapshot.display_snapshot)
+ .into_iter()
+ .map(|selection| selection.range()),
+ );
+ let mut ranges_by_buffer = HashMap::default();
+ self.transact(window, cx, |editor, _window, cx| {
+ for hunk in hunks {
+ if let Some(buffer) = editor.buffer.read(cx).buffer(hunk.buffer_id) {
+ ranges_by_buffer
+ .entry(buffer.clone())
+ .or_insert_with(Vec::new)
+ .push(hunk.buffer_range.to_offset(buffer.read(cx)));
+ }
+ }
+
+ for (buffer, ranges) in ranges_by_buffer {
+ buffer.update(cx, |buffer, cx| {
+ buffer.merge_into_base(ranges, cx);
+ });
+ }
+ });
+
+ if let Some(project) = self.project.clone() {
+ self.save(
+ SaveOptions {
+ format: true,
+ force_format: false,
+ autosave: false,
+ },
+ project,
+ window,
+ cx,
+ )
+ .detach_and_log_err(cx);
+ }
+ }
+
+ pub(super) fn target_file<'a>(&self, cx: &'a App) -> Option<&'a dyn language::LocalFile> {
+ self.active_buffer(cx)?
+ .read(cx)
+ .file()
+ .and_then(|f| f.as_local())
+ }
+
+ pub(super) fn reveal_in_finder(
+ &mut self,
+ _: &RevealInFileManager,
+ _window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ if let Some(path) = self.target_file_abs_path(cx) {
+ if let Some(project) = self.project() {
+ project.update(cx, |project, cx| project.reveal_path(&path, cx));
+ } else {
+ cx.reveal_path(&path);
+ }
+ }
+ }
+
+ pub(super) fn copy_path(
+ &mut self,
+ _: &zed_actions::workspace::CopyPath,
+ _window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ if let Some(path) = self.target_file_abs_path(cx)
+ && let Some(path) = path.to_str()
+ {
+ cx.write_to_clipboard(ClipboardItem::new_string(path.to_string()));
+ } else {
+ cx.propagate();
+ }
+ }
+
+ pub(super) fn copy_relative_path(
+ &mut self,
+ _: &zed_actions::workspace::CopyRelativePath,
+ _window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ if let Some(path) = self.active_buffer(cx).and_then(|buffer| {
+ let project = self.project()?.read(cx);
+ let path = buffer.read(cx).file()?.path();
+ let path = path.display(project.path_style(cx));
+ Some(path)
+ }) {
+ cx.write_to_clipboard(ClipboardItem::new_string(path.to_string()));
+ } else {
+ cx.propagate();
+ }
+ }
+
+ pub(super) fn go_to_active_debug_line(
+ &mut self,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) -> bool {
+ maybe!({
+ let breakpoint_store = self.breakpoint_store.as_ref()?;
+
+ let (active_stack_frame, debug_line_pane_id) = {
+ let store = breakpoint_store.read(cx);
+ let active_stack_frame = store.active_position().cloned();
+ let debug_line_pane_id = store.active_debug_line_pane_id();
+ (active_stack_frame, debug_line_pane_id)
+ };
+
+ let Some(active_stack_frame) = active_stack_frame else {
+ self.clear_row_highlights::<ActiveDebugLine>();
+ return None;
+ };
+
+ if let Some(debug_line_pane_id) = debug_line_pane_id {
+ if let Some(workspace) = self
+ .workspace
+ .as_ref()
+ .and_then(|(workspace, _)| workspace.upgrade())
+ {
+ let editor_pane_id = workspace
+ .read(cx)
+ .pane_for_item_id(cx.entity_id())
+ .map(|pane| pane.entity_id());
+
+ if editor_pane_id.is_some_and(|id| id != debug_line_pane_id) {
+ self.clear_row_highlights::<ActiveDebugLine>();
+ return None;
+ }
+ }
+ }
+
+ let position = active_stack_frame.position;
+
+ let snapshot = self.buffer.read(cx).snapshot(cx);
+ let multibuffer_anchor = snapshot.anchor_in_excerpt(position)?;
+
+ self.clear_row_highlights::<ActiveDebugLine>();
+
+ self.go_to_line::<ActiveDebugLine>(
+ multibuffer_anchor,
+ Some(cx.theme().colors().editor_debugger_active_line_background),
+ window,
+ cx,
+ );
+
+ cx.notify();
+
+ Some(())
+ })
+ .is_some()
+ }
+
+ pub(super) fn open_git_blame_commit(
+ &mut self,
+ _: &OpenGitBlameCommit,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ self.open_git_blame_commit_internal(window, cx);
+ }
+
+ pub(super) fn start_git_blame(
+ &mut self,
+ user_triggered: bool,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ if let Some(project) = self.project() {
+ if let Some(buffer) = self.buffer().read(cx).as_singleton()
+ && buffer.read(cx).file().is_none()
+ {
+ return;
+ }
+
+ let focused = self.focus_handle(cx).contains_focused(window, cx);
+
+ let project = project.clone();
+ let blame = cx
+ .new(|cx| GitBlame::new(self.buffer.clone(), project, user_triggered, focused, cx));
+ self.blame_subscription =
+ Some(cx.observe_in(&blame, window, |_, _, _, cx| cx.notify()));
+ self.blame = Some(blame);
+ }
+ }
+
+ pub(super) fn toggle_git_blame_inline_internal(
+ &mut self,
+ user_triggered: bool,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ if self.git_blame_inline_enabled {
+ self.git_blame_inline_enabled = false;
+ self.show_git_blame_inline = false;
+ self.show_git_blame_inline_delay_task.take();
+ } else {
+ self.git_blame_inline_enabled = true;
+ self.start_git_blame_inline(user_triggered, window, cx);
+ }
+
+ cx.notify();
+ }
+
+ pub(super) fn start_git_blame_inline(
+ &mut self,
+ user_triggered: bool,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ self.start_git_blame(user_triggered, window, cx);
+
+ if ProjectSettings::get_global(cx)
+ .git
+ .inline_blame_delay()
+ .is_some()
+ {
+ self.start_inline_blame_timer(window, cx);
+ } else {
+ self.show_git_blame_inline = true
+ }
+ }
+
+ pub(super) fn render_git_blame_gutter(&self, cx: &App) -> bool {
+ !self.mode().is_minimap() && self.show_git_blame_gutter && self.has_blame_entries(cx)
+ }
+
+ pub(super) fn render_git_blame_inline(&self, window: &Window, cx: &App) -> bool {
+ self.show_git_blame_inline
+ && (self.focus_handle.is_focused(window) || self.inline_blame_popover.is_some())
+ && !self.newest_selection_head_on_empty_line(cx)
+ && self.has_blame_entries(cx)
+ }
+
+ fn has_stageable_diff_hunks_in_ranges(
+ &self,
+ ranges: &[Range<Anchor>],
+ snapshot: &MultiBufferSnapshot,
+ ) -> bool {
+ let mut hunks = self.diff_hunks_in_ranges(ranges, snapshot);
+ hunks.any(|hunk| hunk.status().has_secondary_hunk())
+ }
+
+ fn save_buffers_for_ranges_if_needed(
+ &mut self,
+ ranges: &[Range<Anchor>],
+ cx: &mut Context<Editor>,
+ ) -> Task<Result<()>> {
+ let multibuffer = self.buffer.read(cx);
+ let snapshot = multibuffer.read(cx);
+ let buffer_ids: HashSet<_> = ranges
+ .iter()
+ .flat_map(|range| snapshot.buffer_ids_for_range(range.clone()))
+ .collect();
+ drop(snapshot);
+
+ let mut buffers = HashSet::default();
+ for buffer_id in buffer_ids {
+ if let Some(buffer_entity) = multibuffer.buffer(buffer_id) {
+ let buffer = buffer_entity.read(cx);
+ if buffer.file().is_some_and(|file| file.disk_state().exists()) && buffer.is_dirty()
+ {
+ buffers.insert(buffer_entity);
+ }
+ }
+ }
+
+ if let Some(project) = &self.project {
+ project.update(cx, |project, cx| project.save_buffers(buffers, cx))
+ } else {
+ Task::ready(Ok(()))
+ }
+ }
+
+ fn do_stage_or_unstage_and_next(
+ &mut self,
+ stage: bool,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ let ranges = self.selections.disjoint_anchor_ranges().collect::<Vec<_>>();
+
+ if ranges.iter().any(|range| range.start != range.end) {
+ self.stage_or_unstage_diff_hunks(stage, ranges, cx);
+ return;
+ }
+
+ self.stage_or_unstage_diff_hunks(stage, ranges, cx);
+
+ let all_diff_hunks_expanded = self.buffer().read(cx).all_diff_hunks_expanded();
+ let wrap_around = !all_diff_hunks_expanded;
+ let snapshot = self.snapshot(window, cx);
+ let position = self
+ .selections
+ .newest::<Point>(&snapshot.display_snapshot)
+ .head();
+
+ self.go_to_hunk_before_or_after_position(
+ &snapshot,
+ position,
+ Direction::Next,
+ wrap_around,
+ window,
+ cx,
+ );
+ }
+
+ fn open_git_blame_commit_internal(
+ &mut self,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) -> Option<()> {
+ let blame = self.blame.as_ref()?;
+ let snapshot = self.snapshot(window, cx);
+ let cursor = self
+ .selections
+ .newest::<Point>(&snapshot.display_snapshot)
+ .head();
+ let (buffer, point) = snapshot.buffer_snapshot().point_to_buffer_point(cursor)?;
+ let (_, blame_entry) = blame
+ .update(cx, |blame, cx| {
+ blame
+ .blame_for_rows(
+ &[RowInfo {
+ buffer_id: Some(buffer.remote_id()),
+ buffer_row: Some(point.row),
+ ..Default::default()
+ }],
+ cx,
+ )
+ .next()
+ })
+ .flatten()?;
+ let renderer = cx.global::<GlobalBlameRenderer>().0.clone();
+ let repo = blame.read(cx).repository(cx, buffer.remote_id())?;
+ let workspace = self.workspace()?.downgrade();
+ renderer.open_blame_commit(blame_entry, repo, workspace, window, cx);
+ None
+ }
+
+ fn has_blame_entries(&self, cx: &App) -> bool {
+ self.blame()
+ .is_some_and(|blame| blame.read(cx).has_generated_entries())
+ }
+
+ fn newest_selection_head_on_empty_line(&self, cx: &App) -> bool {
+ let cursor_anchor = self.selections.newest_anchor().head();
+
+ let snapshot = self.buffer.read(cx).snapshot(cx);
+ let buffer_row = MultiBufferRow(cursor_anchor.to_point(&snapshot).row);
+
+ snapshot.line_len(buffer_row) == 0
+ }
+}
@@ -0,0 +1,2221 @@
+use super::*;
+
+const ORDERED_LIST_MAX_MARKER_LEN: usize = 16;
+
+impl Editor {
+ pub fn set_input_enabled(&mut self, input_enabled: bool) {
+ self.input_enabled = input_enabled;
+ }
+
+ pub fn set_expects_character_input(&mut self, expects_character_input: bool) {
+ self.expects_character_input = expects_character_input;
+ }
+
+ pub fn set_autoindent(&mut self, autoindent: bool) {
+ if autoindent {
+ self.autoindent_mode = Some(AutoindentMode::EachLine);
+ } else {
+ self.autoindent_mode = None;
+ }
+ }
+
+ pub fn set_use_autoclose(&mut self, autoclose: bool) {
+ self.use_autoclose = autoclose;
+ }
+
+ pub fn replay_insert_event(
+ &mut self,
+ text: &str,
+ relative_utf16_range: Option<Range<isize>>,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ if !self.input_enabled {
+ cx.emit(EditorEvent::InputIgnored { text: text.into() });
+ return;
+ }
+ if let Some(relative_utf16_range) = relative_utf16_range {
+ let selections = self
+ .selections
+ .all::<MultiBufferOffsetUtf16>(&self.display_snapshot(cx));
+ self.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
+ let new_ranges = selections.into_iter().map(|range| {
+ let start = MultiBufferOffsetUtf16(OffsetUtf16(
+ range
+ .head()
+ .0
+ .0
+ .saturating_add_signed(relative_utf16_range.start),
+ ));
+ let end = MultiBufferOffsetUtf16(OffsetUtf16(
+ range
+ .head()
+ .0
+ .0
+ .saturating_add_signed(relative_utf16_range.end),
+ ));
+ start..end
+ });
+ s.select_ranges(new_ranges);
+ });
+ }
+
+ self.handle_input(text, window, cx);
+ }
+
+ pub fn handle_input(&mut self, text: &str, window: &mut Window, cx: &mut Context<Self>) {
+ let text: Arc<str> = text.into();
+
+ if self.read_only(cx) {
+ return;
+ }
+
+ self.unfold_buffers_with_selections(cx);
+
+ let selections = self.selections.all_adjusted(&self.display_snapshot(cx));
+ let mut bracket_inserted = false;
+ let mut edits = Vec::new();
+ let mut linked_edits = LinkedEdits::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);
+ let mut clear_linked_edit_ranges = false;
+ let mut all_selections_read_only = true;
+ let mut has_adjacent_edits = false;
+ let mut in_adjacent_group = false;
+
+ let mut regions = self
+ .selections_with_autoclose_regions(selections, &snapshot)
+ .peekable();
+
+ while let Some((selection, autoclose_region)) = regions.next() {
+ if snapshot
+ .point_to_buffer_point(selection.head())
+ .is_none_or(|(snapshot, ..)| !snapshot.capability.editable())
+ {
+ continue;
+ }
+ if snapshot
+ .point_to_buffer_point(selection.tail())
+ .is_none_or(|(snapshot, ..)| !snapshot.capability.editable())
+ {
+ // note, ideally we'd clip the tail to the closest writeable region towards the head
+ continue;
+ }
+ all_selections_read_only = false;
+
+ if let Some(scope) = snapshot.language_scope_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;
+ let mut is_bracket_pair_end = false;
+ if !text.is_empty() {
+ let mut bracket_pair_matching_end = None;
+ // `text` can be empty when a user is using IME (e.g. Chinese Wubi Simplified)
+ // and they are removing the character that triggered IME popup.
+ for (pair, enabled) in scope.brackets() {
+ if !pair.close && !pair.surround {
+ continue;
+ }
+
+ if enabled && pair.start.ends_with(text.as_ref()) {
+ let prefix_len = pair.start.len() - text.len();
+ let preceding_text_matches_prefix = prefix_len == 0
+ || (selection.start.column >= (prefix_len as u32)
+ && snapshot.contains_str_at(
+ Point::new(
+ selection.start.row,
+ selection.start.column - (prefix_len as u32),
+ ),
+ &pair.start[..prefix_len],
+ ));
+ if preceding_text_matches_prefix {
+ bracket_pair = Some(pair.clone());
+ is_bracket_pair_start = true;
+ break;
+ }
+ }
+ if pair.end.as_str() == text.as_ref() && bracket_pair_matching_end.is_none()
+ {
+ // take first bracket pair matching end, but don't break in case a later bracket
+ // pair matches start
+ bracket_pair_matching_end = Some(pair.clone());
+ }
+ }
+ if let Some(end) = bracket_pair_matching_end
+ && bracket_pair.is_none()
+ {
+ bracket_pair = Some(end);
+ is_bracket_pair_end = true;
+ }
+ }
+
+ if let Some(bracket_pair) = bracket_pair {
+ let snapshot_settings = snapshot.language_settings_at(selection.start, cx);
+ let autoclose = self.use_autoclose && snapshot_settings.use_autoclose;
+ let auto_surround =
+ self.use_auto_surround && snapshot_settings.use_auto_surround;
+ if selection.is_empty() {
+ if is_bracket_pair_start {
+ // 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 following_text_allows_autoclose = snapshot
+ .chars_at(selection.start)
+ .next()
+ .is_none_or(|c| scope.should_autoclose_before(c));
+
+ let preceding_text_allows_autoclose = selection.start.column == 0
+ || snapshot
+ .reversed_chars_at(selection.start)
+ .next()
+ .is_none_or(|c| {
+ bracket_pair.start != bracket_pair.end
+ || !snapshot
+ .char_classifier_at(selection.start)
+ .is_word(c)
+ });
+
+ let is_closing_quote = if bracket_pair.end == bracket_pair.start
+ && bracket_pair.start.len() == 1
+ {
+ if let Some(target) = bracket_pair.start.chars().next() {
+ let mut byte_offset = 0u32;
+ let current_line_count = snapshot
+ .reversed_chars_at(selection.start)
+ .take_while(|&c| c != '\n')
+ .filter(|c| {
+ byte_offset += c.len_utf8() as u32;
+ if *c != target {
+ return false;
+ }
+
+ let point = Point::new(
+ selection.start.row,
+ selection.start.column.saturating_sub(byte_offset),
+ );
+
+ let is_enabled = snapshot
+ .language_scope_at(point)
+ .and_then(|scope| {
+ scope
+ .brackets()
+ .find(|(pair, _)| {
+ pair.start == bracket_pair.start
+ })
+ .map(|(_, enabled)| enabled)
+ })
+ .unwrap_or(true);
+
+ let is_delimiter = snapshot
+ .language_scope_at(Point::new(
+ point.row,
+ point.column + 1,
+ ))
+ .and_then(|scope| {
+ scope
+ .brackets()
+ .find(|(pair, _)| {
+ pair.start == bracket_pair.start
+ })
+ .map(|(_, enabled)| !enabled)
+ })
+ .unwrap_or(false);
+
+ is_enabled && !is_delimiter
+ })
+ .count();
+ current_line_count % 2 == 1
+ } else {
+ false
+ }
+ } else {
+ false
+ };
+
+ if autoclose
+ && bracket_pair.close
+ && following_text_allows_autoclose
+ && preceding_text_allows_autoclose
+ && !is_closing_quote
+ {
+ let anchor = snapshot.anchor_before(selection.end);
+ new_selections.push((selection.map(|_| anchor), text.len()));
+ new_autoclose_regions.push((
+ anchor,
+ text.len(),
+ selection.id,
+ bracket_pair.clone(),
+ ));
+ edits.push((
+ selection.range(),
+ format!("{}{}", text, bracket_pair.end).into(),
+ ));
+ bracket_inserted = true;
+ continue;
+ }
+ }
+
+ if let Some(region) = autoclose_region {
+ // If the selection is followed by an auto-inserted closing bracket,
+ // then don't insert that closing bracket again; just move the selection
+ // past the closing bracket.
+ let should_skip = selection.end == region.range.end.to_point(&snapshot)
+ && text.as_ref() == region.pair.end.as_str()
+ && snapshot.contains_str_at(region.range.end, text.as_ref());
+ if should_skip {
+ let anchor = snapshot.anchor_after(selection.end);
+ new_selections
+ .push((selection.map(|_| anchor), region.pair.end.len()));
+ continue;
+ }
+ }
+
+ let always_treat_brackets_as_autoclosed = snapshot
+ .language_settings_at(selection.start, cx)
+ .always_treat_brackets_as_autoclosed;
+ if always_treat_brackets_as_autoclosed
+ && is_bracket_pair_end
+ && snapshot.contains_str_at(selection.end, text.as_ref())
+ {
+ // Otherwise, when `always_treat_brackets_as_autoclosed` is set to `true
+ // and the inserted text is a closing bracket and the selection is followed
+ // by the closing bracket then move the selection past the closing bracket.
+ let anchor = snapshot.anchor_after(selection.end);
+ new_selections.push((selection.map(|_| anchor), text.len()));
+ continue;
+ }
+ }
+ // If an opening bracket is 1 character long and is typed while
+ // text is selected, then surround that text with the bracket pair.
+ else if auto_surround
+ && bracket_pair.surround
+ && is_bracket_pair_start
+ && bracket_pair.start.chars().count() == 1
+ {
+ edits.push((selection.start..selection.start, text.clone()));
+ edits.push((
+ selection.end..selection.end,
+ bracket_pair.end.as_str().into(),
+ ));
+ bracket_inserted = true;
+ 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 self.auto_replace_emoji_shortcode
+ && selection.is_empty()
+ && text.as_ref().ends_with(':')
+ && let Some(possible_emoji_short_code) =
+ Self::find_possible_emoji_shortcode_at_position(&snapshot, selection.start)
+ && !possible_emoji_short_code.is_empty()
+ && let Some(emoji) = emojis::get_by_shortcode(&possible_emoji_short_code)
+ {
+ let emoji_shortcode_start = Point::new(
+ selection.start.row,
+ selection.start.column - possible_emoji_short_code.len() as u32 - 1,
+ );
+
+ // Remove shortcode from buffer
+ edits.push((
+ emoji_shortcode_start..selection.start,
+ "".to_string().into(),
+ ));
+ new_selections.push((
+ Selection {
+ id: selection.id,
+ start: snapshot.anchor_after(emoji_shortcode_start),
+ end: snapshot.anchor_before(selection.start),
+ reversed: selection.reversed,
+ goal: selection.goal,
+ },
+ 0,
+ ));
+
+ // Insert emoji
+ let selection_start_anchor = snapshot.anchor_after(selection.start);
+ new_selections.push((selection.map(|_| selection_start_anchor), 0));
+ edits.push((selection.start..selection.end, emoji.to_string().into()));
+
+ continue;
+ }
+
+ let next_is_adjacent = regions
+ .peek()
+ .is_some_and(|(next, _)| selection.end == next.start);
+
+ // 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 = if in_adjacent_group || next_is_adjacent {
+ // After edits the right bias would shift those anchor to the next visible fragment
+ // but we want to resolve to the previous one
+ snapshot.anchor_before(selection.end)
+ } else {
+ snapshot.anchor_after(selection.end)
+ };
+
+ if !self.linked_edit_ranges.is_empty() {
+ let start_anchor = snapshot.anchor_before(selection.start);
+ let classifier = snapshot
+ .char_classifier_at(start_anchor)
+ .scope_context(Some(CharScopeContext::LinkedEdit));
+
+ if let Some((_, anchor_range)) =
+ snapshot.anchor_range_to_buffer_anchor_range(start_anchor..anchor)
+ {
+ let is_word_char = text
+ .chars()
+ .next()
+ .is_none_or(|char| classifier.is_word(char));
+
+ let is_dot = text.as_ref() == ".";
+ let should_apply_linked_edit = is_word_char || is_dot;
+
+ if should_apply_linked_edit {
+ linked_edits.push(&self, anchor_range, text.clone(), cx);
+ } else {
+ clear_linked_edit_ranges = true;
+ }
+ }
+ }
+
+ new_selections.push((selection.map(|_| anchor), 0));
+ edits.push((selection.start..selection.end, text.clone()));
+
+ has_adjacent_edits |= next_is_adjacent;
+ in_adjacent_group = next_is_adjacent;
+ }
+
+ if all_selections_read_only {
+ return;
+ }
+
+ drop(regions);
+ drop(snapshot);
+
+ self.transact(window, cx, |this, window, cx| {
+ if clear_linked_edit_ranges {
+ this.linked_edit_ranges.clear();
+ }
+ let initial_buffer_versions =
+ jsx_tag_auto_close::construct_initial_buffer_versions_map(this, &edits, cx);
+
+ this.buffer.update(cx, |buffer, cx| {
+ if has_adjacent_edits {
+ buffer.edit_non_coalesce(edits, this.autoindent_mode.clone(), cx);
+ } else {
+ buffer.edit(edits, this.autoindent_mode.clone(), cx);
+ }
+ });
+ linked_edits.apply(cx);
+ let new_anchor_selections = new_selections.iter().map(|e| &e.0);
+ let new_selection_deltas = new_selections.iter().map(|e| e.1);
+ let map = this.display_map.update(cx, |map, cx| map.snapshot(cx));
+ let new_selections = resolve_selections_wrapping_blocks::<MultiBufferOffset, _>(
+ new_anchor_selections,
+ &map,
+ )
+ .zip(new_selection_deltas)
+ .map(|(selection, delta)| Selection {
+ id: selection.id,
+ start: selection.start + delta,
+ end: selection.end + delta,
+ reversed: selection.reversed,
+ goal: SelectionGoal::None,
+ })
+ .collect::<Vec<_>>();
+
+ let mut i = 0;
+ for (position, delta, selection_id, pair) in new_autoclose_regions {
+ let position = position.to_offset(map.buffer_snapshot()) + delta;
+ let start = map.buffer_snapshot().anchor_before(position);
+ let end = map.buffer_snapshot().anchor_after(position);
+ while let Some(existing_state) = this.autoclose_regions.get(i) {
+ match existing_state
+ .range
+ .start
+ .cmp(&start, map.buffer_snapshot())
+ {
+ Ordering::Less => i += 1,
+ Ordering::Greater => break,
+ Ordering::Equal => {
+ match end.cmp(&existing_state.range.end, map.buffer_snapshot()) {
+ Ordering::Less => i += 1,
+ Ordering::Equal => break,
+ Ordering::Greater => break,
+ }
+ }
+ }
+ }
+ this.autoclose_regions.insert(
+ i,
+ AutocloseRegion {
+ selection_id,
+ range: start..end,
+ pair,
+ },
+ );
+ }
+
+ let had_active_edit_prediction = this.has_active_edit_prediction();
+ this.change_selections(
+ SelectionEffects::scroll(Autoscroll::fit()).completions(false),
+ window,
+ cx,
+ |s| s.select(new_selections),
+ );
+
+ if !bracket_inserted
+ && let Some(on_type_format_task) =
+ this.trigger_on_type_formatting(text.to_string(), window, cx)
+ {
+ on_type_format_task.detach_and_log_err(cx);
+ }
+
+ let editor_settings = EditorSettings::get_global(cx);
+ if bracket_inserted
+ && (editor_settings.auto_signature_help
+ || editor_settings.show_signature_help_after_edits)
+ {
+ this.show_signature_help(&ShowSignatureHelp, window, cx);
+ }
+
+ let trigger_in_words =
+ this.show_edit_predictions_in_menu() || !had_active_edit_prediction;
+ if this.hard_wrap.is_some() {
+ let latest: Range<Point> = this.selections.newest(&map).range();
+ if latest.is_empty()
+ && this
+ .buffer()
+ .read(cx)
+ .snapshot(cx)
+ .line_len(MultiBufferRow(latest.start.row))
+ == latest.start.column
+ {
+ this.rewrap(
+ RewrapOptions {
+ override_language_settings: true,
+ preserve_existing_whitespace: true,
+ line_length: None,
+ },
+ cx,
+ )
+ }
+ }
+ this.trigger_completion_on_input(&text, trigger_in_words, window, cx);
+ refresh_linked_ranges(this, window, cx);
+ this.refresh_edit_prediction(true, false, window, cx);
+ jsx_tag_auto_close::handle_from(this, initial_buffer_versions, window, cx);
+ });
+ }
+
+ pub fn newline(&mut self, _: &Newline, window: &mut Window, cx: &mut Context<Self>) {
+ if self.read_only(cx) {
+ return;
+ }
+
+ self.transact(window, cx, |this, window, cx| {
+ let (edits_with_flags, selection_info): (Vec<_>, Vec<_>) = {
+ let selections = this
+ .selections
+ .all::<MultiBufferOffset>(&this.display_snapshot(cx));
+ let multi_buffer = this.buffer.read(cx);
+ let buffer = multi_buffer.snapshot(cx);
+ selections
+ .iter()
+ .map(|selection| {
+ let start_point = selection.start.to_point(&buffer);
+ let mut existing_indent =
+ buffer.indent_size_for_line(MultiBufferRow(start_point.row));
+ let full_indent_len = existing_indent.len;
+ existing_indent.len = cmp::min(existing_indent.len, start_point.column);
+ let mut start = selection.start;
+ let end = selection.end;
+ let selection_is_empty = start == end;
+ let language_scope = buffer.language_scope_at(start);
+ let (delimiter, newline_config) = if let Some(language) = &language_scope {
+ let needs_extra_newline = NewlineConfig::insert_extra_newline_brackets(
+ &buffer,
+ start..end,
+ language,
+ )
+ || NewlineConfig::insert_extra_newline_tree_sitter(
+ &buffer,
+ start..end,
+ );
+
+ let mut newline_config = NewlineConfig::Newline {
+ additional_indent: IndentSize::spaces(0),
+ extra_line_additional_indent: if needs_extra_newline {
+ Some(IndentSize::spaces(0))
+ } else {
+ None
+ },
+ prevent_auto_indent: false,
+ };
+
+ let comment_delimiter = maybe!({
+ if !selection_is_empty {
+ return None;
+ }
+
+ if !multi_buffer.language_settings(cx).extend_comment_on_newline {
+ return None;
+ }
+
+ return comment_delimiter_for_newline(
+ &start_point,
+ &buffer,
+ language,
+ );
+ });
+
+ let doc_delimiter = maybe!({
+ if !selection_is_empty {
+ return None;
+ }
+
+ if !multi_buffer.language_settings(cx).extend_comment_on_newline {
+ return None;
+ }
+
+ return documentation_delimiter_for_newline(
+ &start_point,
+ &buffer,
+ language,
+ &mut newline_config,
+ );
+ });
+
+ let list_delimiter = maybe!({
+ if !selection_is_empty {
+ return None;
+ }
+
+ if !multi_buffer.language_settings(cx).extend_list_on_newline {
+ return None;
+ }
+
+ return list_delimiter_for_newline(
+ &start_point,
+ &buffer,
+ language,
+ &mut newline_config,
+ );
+ });
+
+ (
+ comment_delimiter.or(doc_delimiter).or(list_delimiter),
+ newline_config,
+ )
+ } else {
+ (
+ None,
+ NewlineConfig::Newline {
+ additional_indent: IndentSize::spaces(0),
+ extra_line_additional_indent: None,
+ prevent_auto_indent: false,
+ },
+ )
+ };
+
+ let (edit_start, new_text, prevent_auto_indent) = match &newline_config {
+ NewlineConfig::ClearCurrentLine => {
+ let row_start =
+ buffer.point_to_offset(Point::new(start_point.row, 0));
+ (row_start, String::new(), false)
+ }
+ NewlineConfig::UnindentCurrentLine { continuation } => {
+ let row_start =
+ buffer.point_to_offset(Point::new(start_point.row, 0));
+ let tab_size = buffer.language_settings_at(start, cx).tab_size;
+ let tab_size_indent = IndentSize::spaces(tab_size.get());
+ let reduced_indent =
+ existing_indent.with_delta(Ordering::Less, tab_size_indent);
+ let mut new_text = String::new();
+ new_text.extend(reduced_indent.chars());
+ new_text.push_str(continuation);
+ (row_start, new_text, true)
+ }
+ NewlineConfig::Newline {
+ additional_indent,
+ extra_line_additional_indent,
+ prevent_auto_indent,
+ } => {
+ let auto_indent_mode =
+ buffer.language_settings_at(start, cx).auto_indent;
+ let preserve_indent =
+ auto_indent_mode != language::AutoIndentMode::None;
+ let apply_syntax_indent =
+ auto_indent_mode == language::AutoIndentMode::SyntaxAware;
+ let capacity_for_delimiter =
+ delimiter.as_deref().map(str::len).unwrap_or_default();
+ let existing_indent_len = if preserve_indent {
+ existing_indent.len as usize
+ } else {
+ 0
+ };
+ let extra_line_len = extra_line_additional_indent
+ .map(|i| 1 + existing_indent_len + i.len as usize)
+ .unwrap_or(0);
+ let mut new_text = String::with_capacity(
+ 1 + capacity_for_delimiter
+ + existing_indent_len
+ + additional_indent.len as usize
+ + extra_line_len,
+ );
+ new_text.push('\n');
+ if preserve_indent {
+ new_text.extend(existing_indent.chars());
+ }
+ new_text.extend(additional_indent.chars());
+ if let Some(delimiter) = &delimiter {
+ new_text.push_str(delimiter);
+ }
+ if let Some(extra_indent) = extra_line_additional_indent {
+ new_text.push('\n');
+ if preserve_indent {
+ new_text.extend(existing_indent.chars());
+ }
+ new_text.extend(extra_indent.chars());
+ }
+ // Extend the edit to the beginning of the line
+ // to clear auto-indent whitespace that would
+ // otherwise remain as trailing whitespace. This
+ // applies to blank lines and lines where only
+ // indentation remains before the cursor.
+ if selection_is_empty
+ && preserve_indent
+ && full_indent_len > 0
+ && start_point.column == full_indent_len
+ {
+ start = buffer.point_to_offset(Point::new(start_point.row, 0));
+ }
+
+ (
+ start,
+ new_text,
+ *prevent_auto_indent || !apply_syntax_indent,
+ )
+ }
+ };
+
+ let anchor = buffer.anchor_after(end);
+ let new_selection = selection.map(|_| anchor);
+ (
+ ((edit_start..end, new_text), prevent_auto_indent),
+ (newline_config.has_extra_line(), new_selection),
+ )
+ })
+ .unzip()
+ };
+
+ let mut auto_indent_edits = Vec::new();
+ let mut edits = Vec::new();
+ for (edit, prevent_auto_indent) in edits_with_flags {
+ if prevent_auto_indent {
+ edits.push(edit);
+ } else {
+ auto_indent_edits.push(edit);
+ }
+ }
+ if !edits.is_empty() {
+ this.edit(edits, cx);
+ }
+ if !auto_indent_edits.is_empty() {
+ this.edit_with_autoindent(auto_indent_edits, cx);
+ }
+
+ let buffer = this.buffer.read(cx).snapshot(cx);
+ let new_selections = selection_info
+ .into_iter()
+ .map(|(extra_newline_inserted, new_selection)| {
+ let mut cursor = new_selection.end.to_point(&buffer);
+ if extra_newline_inserted {
+ cursor.row -= 1;
+ cursor.column = buffer.line_len(MultiBufferRow(cursor.row));
+ }
+ new_selection.map(|_| cursor)
+ })
+ .collect();
+
+ this.change_selections(Default::default(), window, cx, |s| s.select(new_selections));
+ this.refresh_edit_prediction(true, false, window, cx);
+ if let Some(task) = this.trigger_on_type_formatting("\n".to_owned(), window, cx) {
+ task.detach_and_log_err(cx);
+ }
+ });
+ }
+
+ pub fn newline_above(&mut self, _: &NewlineAbove, window: &mut Window, cx: &mut Context<Self>) {
+ if self.read_only(cx) {
+ return;
+ }
+
+ let buffer = self.buffer.read(cx);
+ let snapshot = buffer.snapshot(cx);
+
+ let mut edits = Vec::new();
+ let mut rows = Vec::new();
+
+ for (rows_inserted, selection) in self
+ .selections
+ .all_adjusted(&self.display_snapshot(cx))
+ .into_iter()
+ .enumerate()
+ {
+ let cursor = selection.head();
+ let row = cursor.row;
+
+ let start_of_line = snapshot.clip_point(Point::new(row, 0), Bias::Left);
+
+ let newline = "\n".to_string();
+ edits.push((start_of_line..start_of_line, newline));
+
+ rows.push(row + rows_inserted as u32);
+ }
+
+ self.transact(window, cx, |editor, window, cx| {
+ editor.edit(edits, cx);
+
+ editor.change_selections(Default::default(), window, cx, |s| {
+ let mut index = 0;
+ s.move_cursors_with(&mut |map, _, _| {
+ let row = rows[index];
+ index += 1;
+
+ let point = Point::new(row, 0);
+ let boundary = map.next_line_boundary(point).1;
+ let clipped = map.clip_point(boundary, Bias::Left);
+
+ (clipped, SelectionGoal::None)
+ });
+ });
+
+ let mut indent_edits = Vec::new();
+ let multibuffer_snapshot = editor.buffer.read(cx).snapshot(cx);
+ for row in rows {
+ let indents = multibuffer_snapshot.suggested_indents(row..row + 1, cx);
+ for (row, indent) in indents {
+ if indent.len == 0 {
+ continue;
+ }
+
+ let text = match indent.kind {
+ IndentKind::Space => " ".repeat(indent.len as usize),
+ IndentKind::Tab => "\t".repeat(indent.len as usize),
+ };
+ let point = Point::new(row.0, 0);
+ indent_edits.push((point..point, text));
+ }
+ }
+ editor.edit(indent_edits, cx);
+ if let Some(format) = editor.trigger_on_type_formatting("\n".to_owned(), window, cx) {
+ format.detach_and_log_err(cx);
+ }
+ });
+ }
+
+ pub fn newline_below(&mut self, _: &NewlineBelow, window: &mut Window, cx: &mut Context<Self>) {
+ if self.read_only(cx) {
+ return;
+ }
+
+ let mut buffer_edits: HashMap<EntityId, (Entity<Buffer>, Vec<Point>)> = HashMap::default();
+ let mut rows = Vec::new();
+ let mut rows_inserted = 0;
+
+ for selection in self.selections.all_adjusted(&self.display_snapshot(cx)) {
+ let cursor = selection.head();
+ let row = cursor.row;
+
+ let point = Point::new(row, 0);
+ let Some((buffer_handle, buffer_point)) =
+ self.buffer.read(cx).point_to_buffer_point(point, cx)
+ else {
+ continue;
+ };
+
+ buffer_edits
+ .entry(buffer_handle.entity_id())
+ .or_insert_with(|| (buffer_handle, Vec::new()))
+ .1
+ .push(buffer_point);
+
+ rows_inserted += 1;
+ rows.push(row + rows_inserted);
+ }
+
+ self.transact(window, cx, |editor, window, cx| {
+ for (_, (buffer_handle, points)) in &buffer_edits {
+ buffer_handle.update(cx, |buffer, cx| {
+ let edits: Vec<_> = points
+ .iter()
+ .map(|point| {
+ let target = Point::new(point.row + 1, 0);
+ let start_of_line = buffer.point_to_offset(target).min(buffer.len());
+ (start_of_line..start_of_line, "\n")
+ })
+ .collect();
+ buffer.edit(edits, None, cx);
+ });
+ }
+
+ editor.change_selections(Default::default(), window, cx, |s| {
+ let mut index = 0;
+ s.move_cursors_with(&mut |map, _, _| {
+ let row = rows[index];
+ index += 1;
+
+ let point = Point::new(row, 0);
+ let boundary = map.next_line_boundary(point).1;
+ let clipped = map.clip_point(boundary, Bias::Left);
+
+ (clipped, SelectionGoal::None)
+ });
+ });
+
+ let mut indent_edits = Vec::new();
+ let multibuffer_snapshot = editor.buffer.read(cx).snapshot(cx);
+ for row in rows {
+ let indents = multibuffer_snapshot.suggested_indents(row..row + 1, cx);
+ for (row, indent) in indents {
+ if indent.len == 0 {
+ continue;
+ }
+
+ let text = match indent.kind {
+ IndentKind::Space => " ".repeat(indent.len as usize),
+ IndentKind::Tab => "\t".repeat(indent.len as usize),
+ };
+ let point = Point::new(row.0, 0);
+ indent_edits.push((point..point, text));
+ }
+ }
+ editor.edit(indent_edits, cx);
+ if let Some(format) = editor.trigger_on_type_formatting("\n".to_owned(), window, cx) {
+ format.detach_and_log_err(cx);
+ }
+ });
+ }
+
+ pub fn insert(&mut self, text: &str, window: &mut Window, cx: &mut Context<Self>) {
+ let autoindent = text.is_empty().not().then(|| AutoindentMode::Block {
+ original_indent_columns: Vec::new(),
+ });
+ self.replace_selections(text, autoindent, window, cx, false);
+ }
+
+ /// Collects linked edits for the current selections, pairing each linked
+ /// range with `text`.
+ pub fn linked_edits_for_selections(&self, text: Arc<str>, cx: &App) -> LinkedEdits {
+ let multibuffer_snapshot = self.buffer().read(cx).snapshot(cx);
+ let mut linked_edits = LinkedEdits::new();
+ if !self.linked_edit_ranges.is_empty() {
+ for selection in self.selections.disjoint_anchors() {
+ let Some((_, range)) =
+ multibuffer_snapshot.anchor_range_to_buffer_anchor_range(selection.range())
+ else {
+ continue;
+ };
+ linked_edits.push(self, range, text.clone(), cx);
+ }
+ }
+ linked_edits
+ }
+
+ /// Deletes the content covered by the current selections and applies
+ /// linked edits.
+ pub fn delete_selections_with_linked_edits(
+ &mut self,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ self.replace_selections("", None, window, cx, true);
+ }
+
+ pub(super) fn observe_pending_input(&mut self, window: &mut Window, cx: &mut Context<Self>) {
+ let mut pending: String = window
+ .pending_input_keystrokes()
+ .into_iter()
+ .flatten()
+ .filter_map(|keystroke| keystroke.key_char.clone())
+ .collect();
+
+ if !self.input_enabled || self.read_only || !self.focus_handle.is_focused(window) {
+ pending = "".to_string();
+ }
+
+ let existing_pending = self
+ .text_highlights(HighlightKey::PendingInput, cx)
+ .map(|(_, ranges)| ranges.to_vec());
+ if existing_pending.is_none() && pending.is_empty() {
+ return;
+ }
+ let transaction =
+ self.transact(window, cx, |this, window, cx| {
+ let selections = this
+ .selections
+ .all::<MultiBufferOffset>(&this.display_snapshot(cx));
+ let edits = selections
+ .iter()
+ .map(|selection| (selection.end..selection.end, pending.clone()));
+ this.edit(edits, cx);
+ this.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
+ s.select_ranges(selections.into_iter().enumerate().map(|(ix, sel)| {
+ sel.start + ix * pending.len()..sel.end + ix * pending.len()
+ }));
+ });
+ if let Some(existing_ranges) = existing_pending {
+ let edits = existing_ranges.iter().map(|range| (range.clone(), ""));
+ this.edit(edits, cx);
+ }
+ });
+
+ let snapshot = self.snapshot(window, cx);
+ let ranges = self
+ .selections
+ .all::<MultiBufferOffset>(&snapshot.display_snapshot)
+ .into_iter()
+ .map(|selection| {
+ snapshot.buffer_snapshot().anchor_after(selection.end)
+ ..snapshot
+ .buffer_snapshot()
+ .anchor_before(selection.end + pending.len())
+ })
+ .collect();
+
+ if pending.is_empty() {
+ self.clear_highlights(HighlightKey::PendingInput, cx);
+ } else {
+ self.highlight_text(
+ HighlightKey::PendingInput,
+ ranges,
+ HighlightStyle {
+ underline: Some(UnderlineStyle {
+ thickness: px(1.),
+ color: None,
+ wavy: false,
+ }),
+ ..Default::default()
+ },
+ cx,
+ );
+ }
+
+ self.ime_transaction = self.ime_transaction.or(transaction);
+ if let Some(transaction) = self.ime_transaction {
+ self.buffer.update(cx, |buffer, cx| {
+ buffer.group_until_transaction(transaction, cx);
+ });
+ }
+
+ if self
+ .text_highlights(HighlightKey::PendingInput, cx)
+ .is_none()
+ {
+ self.ime_transaction.take();
+ }
+ }
+
+ pub(super) fn linked_editing_ranges_for(
+ &self,
+ query_range: Range<text::Anchor>,
+ cx: &App,
+ ) -> Option<HashMap<Entity<Buffer>, Vec<Range<text::Anchor>>>> {
+ use text::ToOffset as TO;
+
+ if self.linked_edit_ranges.is_empty() {
+ return None;
+ }
+ if query_range.start.buffer_id != query_range.end.buffer_id {
+ return None;
+ };
+ let multibuffer_snapshot = self.buffer.read(cx).snapshot(cx);
+ let buffer = self.buffer.read(cx).buffer(query_range.end.buffer_id)?;
+ let buffer_snapshot = buffer.read(cx).snapshot();
+ let (base_range, linked_ranges) = self.linked_edit_ranges.get(
+ buffer_snapshot.remote_id(),
+ query_range.clone(),
+ &buffer_snapshot,
+ )?;
+ // find offset from the start of current range to current cursor position
+ let start_byte_offset = TO::to_offset(&base_range.start, &buffer_snapshot);
+
+ let start_offset = TO::to_offset(&query_range.start, &buffer_snapshot);
+ let start_difference = start_offset - start_byte_offset;
+ let end_offset = TO::to_offset(&query_range.end, &buffer_snapshot);
+ let end_difference = end_offset - start_byte_offset;
+
+ // Current range has associated linked ranges.
+ let mut linked_edits = HashMap::<_, Vec<_>>::default();
+ for range in linked_ranges.iter() {
+ let start_offset = TO::to_offset(&range.start, &buffer_snapshot);
+ let end_offset = start_offset + end_difference;
+ let start_offset = start_offset + start_difference;
+ if start_offset > buffer_snapshot.len() || end_offset > buffer_snapshot.len() {
+ continue;
+ }
+ if self.selections.disjoint_anchor_ranges().any(|s| {
+ let Some((selection_start, _)) =
+ multibuffer_snapshot.anchor_to_buffer_anchor(s.start)
+ else {
+ return false;
+ };
+ let Some((selection_end, _)) = multibuffer_snapshot.anchor_to_buffer_anchor(s.end)
+ else {
+ return false;
+ };
+ if selection_start.buffer_id != query_range.start.buffer_id
+ || selection_end.buffer_id != query_range.end.buffer_id
+ {
+ return false;
+ }
+ TO::to_offset(&selection_start, &buffer_snapshot) <= end_offset
+ && TO::to_offset(&selection_end, &buffer_snapshot) >= start_offset
+ }) {
+ continue;
+ }
+ let start = buffer_snapshot.anchor_after(start_offset);
+ let end = buffer_snapshot.anchor_after(end_offset);
+ linked_edits
+ .entry(buffer.clone())
+ .or_default()
+ .push(start..end);
+ }
+ Some(linked_edits)
+ }
+
+ pub(super) fn marked_text_ranges(
+ &self,
+ cx: &App,
+ ) -> Option<Vec<Range<MultiBufferOffsetUtf16>>> {
+ let snapshot = self.buffer.read(cx).read(cx);
+ let (_, ranges) = self.text_highlights(HighlightKey::InputComposition, cx)?;
+ Some(
+ ranges
+ .iter()
+ .map(move |range| {
+ range.start.to_offset_utf16(&snapshot)..range.end.to_offset_utf16(&snapshot)
+ })
+ .collect(),
+ )
+ }
+
+ /// Replaces the editor's selections with the provided `text`, applying the
+ /// given `autoindent_mode` (`None` will skip autoindentation).
+ ///
+ /// Early returns if the editor is in read-only mode, without applying any
+ /// edits.
+ pub(super) fn replace_selections(
+ &mut self,
+ text: &str,
+ autoindent_mode: Option<AutoindentMode>,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ apply_linked_edits: bool,
+ ) {
+ if self.read_only(cx) {
+ return;
+ }
+
+ let text: Arc<str> = text.into();
+ self.transact(window, cx, |this, window, cx| {
+ let old_selections = this.selections.all_adjusted(&this.display_snapshot(cx));
+ let linked_edits = if apply_linked_edits {
+ this.linked_edits_for_selections(text.clone(), cx)
+ } else {
+ LinkedEdits::new()
+ };
+
+ let selection_anchors = this.buffer.update(cx, |buffer, cx| {
+ let anchors = {
+ let snapshot = buffer.read(cx);
+ old_selections
+ .iter()
+ .map(|s| {
+ let anchor = snapshot.anchor_after(s.head());
+ s.map(|_| anchor)
+ })
+ .collect::<Vec<_>>()
+ };
+ buffer.edit(
+ old_selections
+ .iter()
+ .map(|s| (s.start..s.end, text.clone())),
+ autoindent_mode,
+ cx,
+ );
+ anchors
+ });
+
+ linked_edits.apply(cx);
+
+ this.change_selections(Default::default(), window, cx, |s| {
+ s.select_anchors(selection_anchors);
+ });
+
+ if apply_linked_edits {
+ refresh_linked_ranges(this, window, cx);
+ }
+
+ cx.notify();
+ });
+ }
+
+ /// If any empty selections is touching the start of its innermost containing autoclose
+ /// region, expand it to select the brackets.
+ pub(super) fn select_autoclose_pair(&mut self, window: &mut Window, cx: &mut Context<Self>) {
+ let selections = self
+ .selections
+ .all::<MultiBufferOffset>(&self.display_snapshot(cx));
+ let buffer = self.buffer.read(cx).read(cx);
+ let new_selections = self
+ .selections_with_autoclose_regions(selections, &buffer)
+ .map(|(mut selection, region)| {
+ if !selection.is_empty() {
+ return selection;
+ }
+
+ if let Some(region) = region {
+ let mut range = region.range.to_offset(&buffer);
+ if selection.start == range.start && range.start.0 >= region.pair.start.len() {
+ range.start -= region.pair.start.len();
+ if buffer.contains_str_at(range.start, ®ion.pair.start)
+ && buffer.contains_str_at(range.end, ®ion.pair.end)
+ {
+ range.end += region.pair.end.len();
+ selection.start = range.start;
+ selection.end = range.end;
+
+ return selection;
+ }
+ }
+ }
+
+ let always_treat_brackets_as_autoclosed = buffer
+ .language_settings_at(selection.start, cx)
+ .always_treat_brackets_as_autoclosed;
+
+ if !always_treat_brackets_as_autoclosed {
+ return selection;
+ }
+
+ if let Some(scope) = buffer.language_scope_at(selection.start) {
+ for (pair, enabled) in scope.brackets() {
+ if !enabled || !pair.close {
+ continue;
+ }
+
+ if buffer.contains_str_at(selection.start, &pair.end) {
+ let pair_start_len = pair.start.len();
+ if buffer.contains_str_at(
+ selection.start.saturating_sub_usize(pair_start_len),
+ &pair.start,
+ ) {
+ selection.start -= pair_start_len;
+ selection.end += pair.end.len();
+
+ return selection;
+ }
+ }
+ }
+ }
+
+ selection
+ })
+ .collect();
+
+ drop(buffer);
+ self.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
+ selections.select(new_selections)
+ });
+ }
+
+ /// Remove any autoclose regions that no longer contain their selection or have invalid anchors in ranges.
+ pub(super) fn invalidate_autoclose_regions(
+ &mut self,
+ mut selections: &[Selection<Anchor>],
+ buffer: &MultiBufferSnapshot,
+ ) {
+ self.autoclose_regions.retain(|state| {
+ if !state.range.start.is_valid(buffer) || !state.range.end.is_valid(buffer) {
+ return false;
+ }
+
+ 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;
+ }
+ }
+ false
+ });
+ }
+
+ fn set_use_auto_surround(&mut self, auto_surround: bool) {
+ self.use_auto_surround = auto_surround;
+ }
+
+ fn find_possible_emoji_shortcode_at_position(
+ snapshot: &MultiBufferSnapshot,
+ position: Point,
+ ) -> Option<String> {
+ let mut chars = Vec::new();
+ let mut found_colon = false;
+ for char in snapshot.reversed_chars_at(position).take(100) {
+ // Found a possible emoji shortcode in the middle of the buffer
+ if found_colon {
+ if char.is_whitespace() {
+ chars.reverse();
+ return Some(chars.iter().collect());
+ }
+ // If the previous character is not a whitespace, we are in the middle of a word
+ // and we only want to complete the shortcode if the word is made up of other emojis
+ let mut containing_word = String::new();
+ for ch in snapshot
+ .reversed_chars_at(position)
+ .skip(chars.len() + 1)
+ .take(100)
+ {
+ if ch.is_whitespace() {
+ break;
+ }
+ containing_word.push(ch);
+ }
+ let containing_word = containing_word.chars().rev().collect::<String>();
+ if util::word_consists_of_emojis(containing_word.as_str()) {
+ chars.reverse();
+ return Some(chars.iter().collect());
+ }
+ }
+
+ if char.is_whitespace() || !char.is_ascii() {
+ return None;
+ }
+ if char == ':' {
+ found_colon = true;
+ } else {
+ chars.push(char);
+ }
+ }
+ // Found a possible emoji shortcode at the beginning of the buffer
+ chars.reverse();
+ Some(chars.iter().collect())
+ }
+
+ /// 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<Item = Selection<D>>,
+ buffer: &'a MultiBufferSnapshot,
+ ) -> impl Iterator<Item = (Selection<D>, Option<&'a AutocloseRegion>)> {
+ let mut i = 0;
+ let mut regions = 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) = regions.get(i) {
+ if pair_state.range.end.to_offset(buffer) < range.start {
+ regions = ®ions[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;
+ }
+ }
+
+ (selection, enclosing)
+ })
+ }
+}
+
+#[cfg(any(test, feature = "test-support"))]
+impl Editor {
+ pub fn set_linked_edit_ranges_for_testing(
+ &mut self,
+ ranges: Vec<(Range<Point>, Vec<Range<Point>>)>,
+ cx: &mut Context<Self>,
+ ) -> Option<()> {
+ let Some((buffer, _)) = self
+ .buffer
+ .read(cx)
+ .text_anchor_for_position(self.selections.newest_anchor().start, cx)
+ else {
+ return None;
+ };
+ let buffer = buffer.read(cx);
+ let buffer_id = buffer.remote_id();
+ let mut linked_ranges = Vec::with_capacity(ranges.len());
+ for (base_range, linked_ranges_points) in ranges {
+ let base_anchor =
+ buffer.anchor_before(base_range.start)..buffer.anchor_after(base_range.end);
+ let linked_anchors = linked_ranges_points
+ .into_iter()
+ .map(|range| buffer.anchor_before(range.start)..buffer.anchor_after(range.end))
+ .collect();
+ linked_ranges.push((base_anchor, linked_anchors));
+ }
+ let mut map = HashMap::default();
+ map.insert(buffer_id, linked_ranges);
+ self.linked_edit_ranges = linked_editing_ranges::LinkedEditingRanges(map);
+ Some(())
+ }
+
+ #[cfg(test)]
+ pub(super) fn set_auto_replace_emoji_shortcode(&mut self, auto_replace: bool) {
+ self.auto_replace_emoji_shortcode = auto_replace;
+ }
+}
+
+pub(super) fn is_list_prefix_row(
+ row: MultiBufferRow,
+ buffer: &MultiBufferSnapshot,
+ language: &LanguageScope,
+) -> bool {
+ let Some((snapshot, range)) = buffer.buffer_line_for_row(row) else {
+ return false;
+ };
+
+ let num_of_whitespaces = snapshot
+ .chars_for_range(range.clone())
+ .take_while(|c| c.is_whitespace())
+ .count();
+
+ let task_list_prefixes: Vec<_> = language
+ .task_list()
+ .into_iter()
+ .flat_map(|config| {
+ config
+ .prefixes
+ .iter()
+ .map(|p| p.as_ref())
+ .collect::<Vec<_>>()
+ })
+ .collect();
+ let unordered_list_markers: Vec<_> = language
+ .unordered_list()
+ .iter()
+ .map(|marker| marker.as_ref())
+ .collect();
+ let all_prefixes: Vec<_> = task_list_prefixes
+ .into_iter()
+ .chain(unordered_list_markers)
+ .collect();
+ if let Some(max_prefix_len) = all_prefixes.iter().map(|p| p.len()).max() {
+ let candidate: String = snapshot
+ .chars_for_range(range.clone())
+ .skip(num_of_whitespaces)
+ .take(max_prefix_len)
+ .collect();
+ if all_prefixes
+ .iter()
+ .any(|prefix| candidate.starts_with(*prefix))
+ {
+ return true;
+ }
+ }
+
+ let ordered_list_candidate: String = snapshot
+ .chars_for_range(range)
+ .skip(num_of_whitespaces)
+ .take(ORDERED_LIST_MAX_MARKER_LEN)
+ .collect();
+ for ordered_config in language.ordered_list() {
+ let regex = match Regex::new(&ordered_config.pattern) {
+ Ok(r) => r,
+ Err(_) => continue,
+ };
+ if let Some(captures) = regex.captures(&ordered_list_candidate) {
+ return captures.get(0).is_some();
+ }
+ }
+
+ false
+}
+
+#[derive(Debug)]
+enum NewlineConfig {
+ /// Insert newline with optional additional indent and optional extra blank line
+ Newline {
+ additional_indent: IndentSize,
+ extra_line_additional_indent: Option<IndentSize>,
+ prevent_auto_indent: bool,
+ },
+ /// Clear the current line
+ ClearCurrentLine,
+ /// Unindent the current line and add continuation
+ UnindentCurrentLine { continuation: Arc<str> },
+}
+
+impl NewlineConfig {
+ fn has_extra_line(&self) -> bool {
+ matches!(
+ self,
+ Self::Newline {
+ extra_line_additional_indent: Some(_),
+ ..
+ }
+ )
+ }
+
+ fn insert_extra_newline_brackets(
+ buffer: &MultiBufferSnapshot,
+ range: Range<MultiBufferOffset>,
+ language: &language::LanguageScope,
+ ) -> bool {
+ let leading_whitespace_len = buffer
+ .reversed_chars_at(range.start)
+ .take_while(|c| c.is_whitespace() && *c != '\n')
+ .map(|c| c.len_utf8())
+ .sum::<usize>();
+ let trailing_whitespace_len = buffer
+ .chars_at(range.end)
+ .take_while(|c| c.is_whitespace() && *c != '\n')
+ .map(|c| c.len_utf8())
+ .sum::<usize>();
+ let range = range.start - leading_whitespace_len..range.end + trailing_whitespace_len;
+
+ language.brackets().any(|(pair, enabled)| {
+ let pair_start = pair.start.trim_end();
+ let pair_end = pair.end.trim_start();
+
+ enabled
+ && pair.newline
+ && buffer.contains_str_at(range.end, pair_end)
+ && buffer.contains_str_at(
+ range.start.saturating_sub_usize(pair_start.len()),
+ pair_start,
+ )
+ })
+ }
+
+ fn insert_extra_newline_tree_sitter(
+ buffer: &MultiBufferSnapshot,
+ range: Range<MultiBufferOffset>,
+ ) -> bool {
+ let (buffer, range) = match buffer
+ .range_to_buffer_ranges(range.start..range.end)
+ .as_slice()
+ {
+ [(buffer_snapshot, range, _)] => (buffer_snapshot.clone(), range.clone()),
+ _ => return false,
+ };
+ let pair = {
+ let mut result: Option<BracketMatch<usize>> = None;
+
+ for pair in buffer
+ .all_bracket_ranges(range.start.0..range.end.0)
+ .filter(move |pair| {
+ pair.open_range.start <= range.start.0 && pair.close_range.end >= range.end.0
+ })
+ {
+ let len = pair.close_range.end - pair.open_range.start;
+
+ if let Some(existing) = &result {
+ let existing_len = existing.close_range.end - existing.open_range.start;
+ if len > existing_len {
+ continue;
+ }
+ }
+
+ result = Some(pair);
+ }
+
+ result
+ };
+ let Some(pair) = pair else {
+ return false;
+ };
+ pair.newline_only
+ && buffer
+ .chars_for_range(pair.open_range.end..range.start.0)
+ .chain(buffer.chars_for_range(range.end.0..pair.close_range.start))
+ .all(|c| c.is_whitespace() && c != '\n')
+ }
+}
+
+fn comment_delimiter_for_newline(
+ start_point: &Point,
+ buffer: &MultiBufferSnapshot,
+ language: &LanguageScope,
+) -> Option<Arc<str>> {
+ let delimiters = language.line_comment_prefixes();
+ let max_len_of_delimiter = delimiters.iter().map(|delimiter| delimiter.len()).max()?;
+ let (snapshot, range) = buffer.buffer_line_for_row(MultiBufferRow(start_point.row))?;
+
+ let num_of_whitespaces = snapshot
+ .chars_for_range(range.clone())
+ .take_while(|c| c.is_whitespace())
+ .count();
+ let comment_candidate = snapshot
+ .chars_for_range(range.clone())
+ .skip(num_of_whitespaces)
+ .take(max_len_of_delimiter + 2)
+ .collect::<String>();
+ let (delimiter, trimmed_len, is_repl) = delimiters
+ .iter()
+ .filter_map(|delimiter| {
+ let prefix = delimiter.trim_end();
+ if comment_candidate.starts_with(prefix) {
+ let is_repl = if let Some(stripped_comment) = comment_candidate.strip_prefix(prefix)
+ {
+ stripped_comment.starts_with(" %%")
+ } else {
+ false
+ };
+ Some((delimiter, prefix.len(), is_repl))
+ } else {
+ None
+ }
+ })
+ .max_by_key(|(_, len, _)| *len)?;
+
+ if let Some(BlockCommentConfig {
+ start: block_start, ..
+ }) = language.block_comment()
+ {
+ let block_start_trimmed = block_start.trim_end();
+ if block_start_trimmed.starts_with(delimiter.trim_end()) {
+ let line_content = snapshot
+ .chars_for_range(range.clone())
+ .skip(num_of_whitespaces)
+ .take(block_start_trimmed.len())
+ .collect::<String>();
+
+ if line_content.starts_with(block_start_trimmed) {
+ return None;
+ }
+ }
+ }
+
+ let cursor_is_placed_after_comment_marker =
+ num_of_whitespaces + trimmed_len <= start_point.column as usize;
+ if cursor_is_placed_after_comment_marker {
+ if !is_repl {
+ return Some(delimiter.clone());
+ }
+
+ let line_content_after_cursor: String = snapshot
+ .chars_for_range(range)
+ .skip(start_point.column as usize)
+ .collect();
+
+ if line_content_after_cursor.trim().is_empty() {
+ return None;
+ } else {
+ return Some(delimiter.clone());
+ }
+ } else {
+ None
+ }
+}
+
+fn documentation_delimiter_for_newline(
+ start_point: &Point,
+ buffer: &MultiBufferSnapshot,
+ language: &LanguageScope,
+ newline_config: &mut NewlineConfig,
+) -> Option<Arc<str>> {
+ let BlockCommentConfig {
+ start: start_tag,
+ end: end_tag,
+ prefix: delimiter,
+ tab_size: len,
+ } = language.documentation_comment()?;
+ let is_within_block_comment = buffer
+ .language_scope_at(*start_point)
+ .is_some_and(|scope| scope.override_name() == Some("comment"));
+ if !is_within_block_comment {
+ return None;
+ }
+
+ let (snapshot, range) = buffer.buffer_line_for_row(MultiBufferRow(start_point.row))?;
+
+ let num_of_whitespaces = snapshot
+ .chars_for_range(range.clone())
+ .take_while(|c| c.is_whitespace())
+ .count();
+
+ // It is safe to use a column from MultiBufferPoint in context of a single buffer ranges, because we're only ever looking at a single line at a time.
+ let column = start_point.column;
+ let cursor_is_after_start_tag = {
+ let start_tag_len = start_tag.len();
+ let start_tag_line = snapshot
+ .chars_for_range(range.clone())
+ .skip(num_of_whitespaces)
+ .take(start_tag_len)
+ .collect::<String>();
+ if start_tag_line.starts_with(start_tag.as_ref()) {
+ num_of_whitespaces + start_tag_len <= column as usize
+ } else {
+ false
+ }
+ };
+
+ let cursor_is_after_delimiter = {
+ let delimiter_trim = delimiter.trim_end();
+ let delimiter_line = snapshot
+ .chars_for_range(range.clone())
+ .skip(num_of_whitespaces)
+ .take(delimiter_trim.len())
+ .collect::<String>();
+ if delimiter_line.starts_with(delimiter_trim) {
+ num_of_whitespaces + delimiter_trim.len() <= column as usize
+ } else {
+ false
+ }
+ };
+
+ let mut needs_extra_line = false;
+ let mut extra_line_additional_indent = IndentSize::spaces(0);
+
+ let cursor_is_before_end_tag_if_exists = {
+ let mut char_position = 0u32;
+ let mut end_tag_offset = None;
+
+ 'outer: for chunk in snapshot.text_for_range(range) {
+ if let Some(byte_pos) = chunk.find(&**end_tag) {
+ let chars_before_match = chunk[..byte_pos].chars().count() as u32;
+ end_tag_offset = Some(char_position + chars_before_match);
+ break 'outer;
+ }
+ char_position += chunk.chars().count() as u32;
+ }
+
+ if let Some(end_tag_offset) = end_tag_offset {
+ let cursor_is_before_end_tag = column <= end_tag_offset;
+ if cursor_is_after_start_tag {
+ if cursor_is_before_end_tag {
+ needs_extra_line = true;
+ }
+ let cursor_is_at_start_of_end_tag = column == end_tag_offset;
+ if cursor_is_at_start_of_end_tag {
+ extra_line_additional_indent.len = *len;
+ }
+ }
+ cursor_is_before_end_tag
+ } else {
+ true
+ }
+ };
+
+ if (cursor_is_after_start_tag || cursor_is_after_delimiter)
+ && cursor_is_before_end_tag_if_exists
+ {
+ let additional_indent = if cursor_is_after_start_tag {
+ IndentSize::spaces(*len)
+ } else {
+ IndentSize::spaces(0)
+ };
+
+ *newline_config = NewlineConfig::Newline {
+ additional_indent,
+ extra_line_additional_indent: if needs_extra_line {
+ Some(extra_line_additional_indent)
+ } else {
+ None
+ },
+ prevent_auto_indent: true,
+ };
+ Some(delimiter.clone())
+ } else {
+ None
+ }
+}
+
+fn list_delimiter_for_newline(
+ start_point: &Point,
+ buffer: &MultiBufferSnapshot,
+ language: &LanguageScope,
+ newline_config: &mut NewlineConfig,
+) -> Option<Arc<str>> {
+ let (snapshot, range) = buffer.buffer_line_for_row(MultiBufferRow(start_point.row))?;
+
+ let num_of_whitespaces = snapshot
+ .chars_for_range(range.clone())
+ .take_while(|c| c.is_whitespace())
+ .count();
+
+ let task_list_entries: Vec<_> = language
+ .task_list()
+ .into_iter()
+ .flat_map(|config| {
+ config
+ .prefixes
+ .iter()
+ .map(|prefix| (prefix.as_ref(), config.continuation.as_ref()))
+ })
+ .collect();
+ let unordered_list_entries: Vec<_> = language
+ .unordered_list()
+ .iter()
+ .map(|marker| (marker.as_ref(), marker.as_ref()))
+ .collect();
+
+ let all_entries: Vec<_> = task_list_entries
+ .into_iter()
+ .chain(unordered_list_entries)
+ .collect();
+
+ if let Some(max_prefix_len) = all_entries.iter().map(|(p, _)| p.len()).max() {
+ let candidate: String = snapshot
+ .chars_for_range(range.clone())
+ .skip(num_of_whitespaces)
+ .take(max_prefix_len)
+ .collect();
+
+ if let Some((prefix, continuation)) = all_entries
+ .iter()
+ .filter(|(prefix, _)| candidate.starts_with(*prefix))
+ .max_by_key(|(prefix, _)| prefix.len())
+ {
+ let end_of_prefix = num_of_whitespaces + prefix.len();
+ let cursor_is_after_prefix = end_of_prefix <= start_point.column as usize;
+ let has_content_after_marker = snapshot
+ .chars_for_range(range)
+ .skip(end_of_prefix)
+ .any(|c| !c.is_whitespace());
+
+ if has_content_after_marker && cursor_is_after_prefix {
+ return Some((*continuation).into());
+ }
+
+ if start_point.column as usize == end_of_prefix {
+ if num_of_whitespaces == 0 {
+ *newline_config = NewlineConfig::ClearCurrentLine;
+ } else {
+ *newline_config = NewlineConfig::UnindentCurrentLine {
+ continuation: (*continuation).into(),
+ };
+ }
+ }
+
+ return None;
+ }
+ }
+
+ let candidate: String = snapshot
+ .chars_for_range(range.clone())
+ .skip(num_of_whitespaces)
+ .take(ORDERED_LIST_MAX_MARKER_LEN)
+ .collect();
+
+ for ordered_config in language.ordered_list() {
+ let regex = match Regex::new(&ordered_config.pattern) {
+ Ok(r) => r,
+ Err(_) => continue,
+ };
+
+ if let Some(captures) = regex.captures(&candidate) {
+ let full_match = captures.get(0)?;
+ let marker_len = full_match.len();
+ let end_of_prefix = num_of_whitespaces + marker_len;
+ let cursor_is_after_prefix = end_of_prefix <= start_point.column as usize;
+
+ let has_content_after_marker = snapshot
+ .chars_for_range(range)
+ .skip(end_of_prefix)
+ .any(|c| !c.is_whitespace());
+
+ if has_content_after_marker && cursor_is_after_prefix {
+ let number: u32 = captures.get(1)?.as_str().parse().ok()?;
+ let continuation = ordered_config
+ .format
+ .replace("{1}", &(number + 1).to_string());
+ return Some(continuation.into());
+ }
+
+ if start_point.column as usize == end_of_prefix {
+ let continuation = ordered_config.format.replace("{1}", "1");
+ if num_of_whitespaces == 0 {
+ *newline_config = NewlineConfig::ClearCurrentLine;
+ } else {
+ *newline_config = NewlineConfig::UnindentCurrentLine {
+ continuation: continuation.into(),
+ };
+ }
+ }
+
+ return None;
+ }
+ }
+
+ None
+}
+
+impl EntityInputHandler for Editor {
+ fn text_for_range(
+ &mut self,
+ range_utf16: Range<usize>,
+ adjusted_range: &mut Option<Range<usize>>,
+ _: &mut Window,
+ cx: &mut Context<Self>,
+ ) -> Option<String> {
+ let snapshot = self.buffer.read(cx).read(cx);
+ let start = snapshot.clip_offset_utf16(
+ MultiBufferOffsetUtf16(OffsetUtf16(range_utf16.start)),
+ Bias::Left,
+ );
+ let end = snapshot.clip_offset_utf16(
+ MultiBufferOffsetUtf16(OffsetUtf16(range_utf16.end)),
+ Bias::Right,
+ );
+ if (start.0.0..end.0.0) != range_utf16 {
+ adjusted_range.replace(start.0.0..end.0.0);
+ }
+ Some(snapshot.text_for_range(start..end).collect())
+ }
+
+ fn selected_text_range(
+ &mut self,
+ ignore_disabled_input: bool,
+ _: &mut Window,
+ cx: &mut Context<Self>,
+ ) -> Option<UTF16Selection> {
+ // Prevent the IME menu from appearing when holding down an alphabetic key
+ // while input is disabled.
+ if !ignore_disabled_input && !self.input_enabled {
+ return None;
+ }
+
+ let selection = self
+ .selections
+ .newest::<MultiBufferOffsetUtf16>(&self.display_snapshot(cx));
+ let range = selection.range();
+
+ Some(UTF16Selection {
+ range: range.start.0.0..range.end.0.0,
+ reversed: selection.reversed,
+ })
+ }
+
+ fn marked_text_range(&self, _: &mut Window, cx: &mut Context<Self>) -> Option<Range<usize>> {
+ let snapshot = self.buffer.read(cx).read(cx);
+ let range = self
+ .text_highlights(HighlightKey::InputComposition, cx)?
+ .1
+ .first()?;
+ Some(range.start.to_offset_utf16(&snapshot).0.0..range.end.to_offset_utf16(&snapshot).0.0)
+ }
+
+ fn unmark_text(&mut self, _: &mut Window, cx: &mut Context<Self>) {
+ self.clear_highlights(HighlightKey::InputComposition, cx);
+ self.ime_transaction.take();
+ }
+
+ fn replace_text_in_range(
+ &mut self,
+ range_utf16: Option<Range<usize>>,
+ text: &str,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ if !self.input_enabled {
+ cx.emit(EditorEvent::InputIgnored { text: text.into() });
+ return;
+ }
+
+ self.transact(window, cx, |this, window, cx| {
+ let new_selected_ranges = if let Some(range_utf16) = range_utf16 {
+ if let Some(marked_ranges) = this.marked_text_ranges(cx) {
+ // During IME composition, macOS reports the replacement range
+ // relative to the first marked region (the only one visible via
+ // marked_text_range). The correct targets for replacement are the
+ // marked ranges themselves — one per cursor — so use them directly.
+ Some(marked_ranges)
+ } else if range_utf16.start == range_utf16.end {
+ // An empty replacement range means "insert at cursor" with no text
+ // to replace. macOS reports the cursor position from its own
+ // (single-cursor) view of the buffer, which diverges from our actual
+ // cursor positions after multi-cursor edits have shifted offsets.
+ // Treating this as range_utf16=None lets each cursor insert in place.
+ None
+ } else {
+ // Outside of IME composition (e.g. Accessibility Keyboard word
+ // completion), the range is an absolute document offset for the
+ // newest cursor. Fan it out to all cursors via
+ // selection_replacement_ranges, which applies the delta relative
+ // to the newest selection to every cursor.
+ let range_utf16 = MultiBufferOffsetUtf16(OffsetUtf16(range_utf16.start))
+ ..MultiBufferOffsetUtf16(OffsetUtf16(range_utf16.end));
+ Some(this.selection_replacement_ranges(range_utf16, cx))
+ }
+ } else {
+ this.marked_text_ranges(cx)
+ };
+
+ let range_to_replace = new_selected_ranges.as_ref().and_then(|ranges_to_replace| {
+ let newest_selection_id = this.selections.newest_anchor().id;
+ this.selections
+ .all::<MultiBufferOffsetUtf16>(&this.display_snapshot(cx))
+ .iter()
+ .zip(ranges_to_replace.iter())
+ .find_map(|(selection, range)| {
+ if selection.id == newest_selection_id {
+ Some(
+ (range.start.0.0 as isize - selection.head().0.0 as isize)
+ ..(range.end.0.0 as isize - selection.head().0.0 as isize),
+ )
+ } else {
+ None
+ }
+ })
+ });
+
+ cx.emit(EditorEvent::InputHandled {
+ utf16_range_to_replace: range_to_replace,
+ text: text.into(),
+ });
+
+ if let Some(new_selected_ranges) = new_selected_ranges {
+ // Only backspace if at least one range covers actual text. When all
+ // ranges are empty (e.g. a trailing-space insertion from Accessibility
+ // Keyboard sends replacementRange=cursor..cursor), backspace would
+ // incorrectly delete the character just before the cursor.
+ let should_backspace = new_selected_ranges.iter().any(|r| r.start != r.end);
+ this.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
+ selections.select_ranges(new_selected_ranges)
+ });
+ if should_backspace {
+ this.backspace(&Default::default(), window, cx);
+ }
+ }
+
+ this.handle_input(text, window, cx);
+ });
+
+ if let Some(transaction) = self.ime_transaction {
+ self.buffer.update(cx, |buffer, cx| {
+ buffer.group_until_transaction(transaction, cx);
+ });
+ }
+
+ self.unmark_text(window, cx);
+ }
+
+ fn replace_and_mark_text_in_range(
+ &mut self,
+ range_utf16: Option<Range<usize>>,
+ text: &str,
+ new_selected_range_utf16: Option<Range<usize>>,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ if !self.input_enabled {
+ return;
+ }
+
+ let transaction = self.transact(window, cx, |this, window, cx| {
+ let ranges_to_replace = if let Some(mut marked_ranges) = this.marked_text_ranges(cx) {
+ let snapshot = this.buffer.read(cx).read(cx);
+ if let Some(relative_range_utf16) = range_utf16.as_ref() {
+ for marked_range in &mut marked_ranges {
+ marked_range.end = marked_range.start + relative_range_utf16.end;
+ marked_range.start += relative_range_utf16.start;
+ marked_range.start =
+ snapshot.clip_offset_utf16(marked_range.start, Bias::Left);
+ marked_range.end =
+ snapshot.clip_offset_utf16(marked_range.end, Bias::Right);
+ }
+ }
+ Some(marked_ranges)
+ } else if let Some(range_utf16) = range_utf16 {
+ let range_utf16 = MultiBufferOffsetUtf16(OffsetUtf16(range_utf16.start))
+ ..MultiBufferOffsetUtf16(OffsetUtf16(range_utf16.end));
+ Some(this.selection_replacement_ranges(range_utf16, cx))
+ } else {
+ None
+ };
+
+ let range_to_replace = ranges_to_replace.as_ref().and_then(|ranges_to_replace| {
+ let newest_selection_id = this.selections.newest_anchor().id;
+ this.selections
+ .all::<MultiBufferOffsetUtf16>(&this.display_snapshot(cx))
+ .iter()
+ .zip(ranges_to_replace.iter())
+ .find_map(|(selection, range)| {
+ if selection.id == newest_selection_id {
+ Some(
+ (range.start.0.0 as isize - selection.head().0.0 as isize)
+ ..(range.end.0.0 as isize - selection.head().0.0 as isize),
+ )
+ } else {
+ None
+ }
+ })
+ });
+
+ cx.emit(EditorEvent::InputHandled {
+ utf16_range_to_replace: range_to_replace,
+ text: text.into(),
+ });
+
+ if let Some(ranges) = ranges_to_replace {
+ this.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
+ s.select_ranges(ranges)
+ });
+ }
+
+ let marked_ranges = {
+ let snapshot = this.buffer.read(cx).read(cx);
+ this.selections
+ .disjoint_anchors_arc()
+ .iter()
+ .map(|selection| {
+ selection.start.bias_left(&snapshot)..selection.end.bias_right(&snapshot)
+ })
+ .collect::<Vec<_>>()
+ };
+
+ if text.is_empty() {
+ this.unmark_text(window, cx);
+ } else {
+ this.highlight_text(
+ HighlightKey::InputComposition,
+ marked_ranges.clone(),
+ HighlightStyle {
+ underline: Some(UnderlineStyle {
+ thickness: px(1.),
+ color: None,
+ wavy: false,
+ }),
+ ..Default::default()
+ },
+ cx,
+ );
+ }
+
+ // Disable auto-closing when composing text (i.e. typing a `"` on a Brazilian keyboard)
+ let use_autoclose = this.use_autoclose;
+ let use_auto_surround = this.use_auto_surround;
+ this.set_use_autoclose(false);
+ this.set_use_auto_surround(false);
+ this.handle_input(text, window, cx);
+ this.set_use_autoclose(use_autoclose);
+ this.set_use_auto_surround(use_auto_surround);
+
+ if let Some(new_selected_range) = new_selected_range_utf16 {
+ let snapshot = this.buffer.read(cx).read(cx);
+ let new_selected_ranges = marked_ranges
+ .into_iter()
+ .map(|marked_range| {
+ let insertion_start = marked_range.start.to_offset_utf16(&snapshot).0;
+ let new_start = MultiBufferOffsetUtf16(OffsetUtf16(
+ insertion_start.0 + new_selected_range.start,
+ ));
+ let new_end = MultiBufferOffsetUtf16(OffsetUtf16(
+ insertion_start.0 + new_selected_range.end,
+ ));
+ snapshot.clip_offset_utf16(new_start, Bias::Left)
+ ..snapshot.clip_offset_utf16(new_end, Bias::Right)
+ })
+ .collect::<Vec<_>>();
+
+ drop(snapshot);
+ this.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
+ selections.select_ranges(new_selected_ranges)
+ });
+ }
+ });
+
+ self.ime_transaction = self.ime_transaction.or(transaction);
+ if let Some(transaction) = self.ime_transaction {
+ self.buffer.update(cx, |buffer, cx| {
+ buffer.group_until_transaction(transaction, cx);
+ });
+ }
+
+ if self
+ .text_highlights(HighlightKey::InputComposition, cx)
+ .is_none()
+ {
+ self.ime_transaction.take();
+ }
+ }
+
+ fn bounds_for_range(
+ &mut self,
+ range_utf16: Range<usize>,
+ element_bounds: gpui::Bounds<Pixels>,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) -> Option<gpui::Bounds<Pixels>> {
+ let text_layout_details = self.text_layout_details(window, cx);
+ let CharacterDimensions {
+ em_width,
+ em_advance,
+ line_height,
+ } = self.character_dimensions(window, cx);
+
+ let snapshot = self.snapshot(window, cx);
+ let scroll_position = snapshot.scroll_position();
+ let scroll_left = scroll_position.x * ScrollOffset::from(em_advance);
+
+ let start =
+ MultiBufferOffsetUtf16(OffsetUtf16(range_utf16.start)).to_display_point(&snapshot);
+ let x = Pixels::from(
+ ScrollOffset::from(
+ snapshot.x_for_display_point(start, &text_layout_details)
+ + self.gutter_dimensions.full_width(),
+ ) - scroll_left,
+ );
+ let y = line_height * (start.row().as_f64() - scroll_position.y) as f32;
+
+ Some(Bounds {
+ origin: element_bounds.origin + point(x, y),
+ size: size(em_width, line_height),
+ })
+ }
+
+ fn character_index_for_point(
+ &mut self,
+ point: gpui::Point<Pixels>,
+ _window: &mut Window,
+ _cx: &mut Context<Self>,
+ ) -> Option<usize> {
+ let position_map = self.last_position_map.as_ref()?;
+ if !position_map.text_hitbox.contains(&point) {
+ return None;
+ }
+ let display_point = position_map.point_for_position(point).previous_valid;
+ let anchor = position_map
+ .snapshot
+ .display_point_to_anchor(display_point, Bias::Left);
+ let utf16_offset = anchor.to_offset_utf16(&position_map.snapshot.buffer_snapshot());
+ Some(utf16_offset.0.0)
+ }
+
+ fn accepts_text_input(&self, _window: &mut Window, _cx: &mut Context<Self>) -> bool {
+ self.expects_character_input
+ }
+}