diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 608895da9c984a5ee76d804f1ff1b135dd626e8d..7147677d91e47cb19f9fcc037710028e12a420e8 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -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, - ) -> 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, - cx: &App, - ) -> Option, Vec>>> { - 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) { - let text: Arc = 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::( - 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::>(); - - 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 = 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 { - 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::(); - 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) { - 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::(&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) { - 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) { - if self.read_only(cx) { - return; - } - - let mut buffer_edits: HashMap, Vec)> = 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) { - 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, - window: &mut Window, - cx: &mut Context, - apply_linked_edits: bool, - ) { - if self.read_only(cx) { - return; - } - - let text: Arc = 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::>() - }; - 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, 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.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, Vec>)>, - cx: &mut Context, - ) -> 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) { - let selections = self - .selections - .all::(&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>, - buffer: &'a MultiBufferSnapshot, - ) -> impl Iterator, 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, + ) -> 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], - 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( @@ -8835,7 +7589,11 @@ impl Editor { let settings = buffer.language_settings_at(cursor, cx); if settings.indent_list_on_tab { if let Some(language) = snapshot.language_scope_at(Point::new(cursor.row, 0)) { - if is_list_prefix_row(MultiBufferRow(cursor.row), &snapshot, &language) { + if input::is_list_prefix_row( + MultiBufferRow(cursor.row), + &snapshot, + &language, + ) { row_delta = Self::indent_selection( buffer, &snapshot, selection, &mut edits, row_delta, cx, ); @@ -17343,387 +16101,39 @@ impl Editor { pub fn expand_all_diff_hunks( &mut self, - _: &ExpandAllDiffHunks, - _window: &mut Window, - cx: &mut Context, - ) { - self.buffer.update(cx, |buffer, cx| { - buffer.expand_diff_hunks(vec![Anchor::Min..Anchor::Max], cx) - }); - } - - pub fn collapse_all_diff_hunks( - &mut self, - _: &CollapseAllDiffHunks, - _window: &mut Window, - cx: &mut Context, - ) { - self.buffer.update(cx, |buffer, cx| { - buffer.collapse_diff_hunks(vec![Anchor::Min..Anchor::Max], cx) - }); - } - - pub fn toggle_selected_diff_hunks( - &mut self, - _: &ToggleSelectedDiffHunks, - _window: &mut Window, - cx: &mut Context, - ) { - let ranges: Vec<_> = self - .selections - .disjoint_anchors() - .iter() - .map(|s| s.range()) - .collect(); - self.toggle_diff_hunks_in_ranges(ranges, cx); - } - - pub fn diff_hunks_in_ranges<'a>( - &'a self, - ranges: &'a [Range], - buffer: &'a MultiBufferSnapshot, - ) -> impl 'a + Iterator { - 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 has_stageable_diff_hunks_in_ranges( - &self, - ranges: &[Range], - snapshot: &MultiBufferSnapshot, - ) -> bool { - let mut hunks = self.diff_hunks_in_ranges(ranges, snapshot); - hunks.any(|hunk| hunk.status().has_secondary_hunk()) - } - - pub fn toggle_staged_selected_diff_hunks( - &mut self, - _: &::git::ToggleStaged, - _: &mut Window, - cx: &mut Context, - ) { - 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 fn set_render_diff_hunk_controls( - &mut self, - render_diff_hunk_controls: RenderDiffHunkControlsFn, - cx: &mut Context, - ) { - self.render_diff_hunk_controls = render_diff_hunk_controls; - cx.notify(); - } - - pub fn stage_and_next( - &mut self, - _: &::git::StageAndNext, - window: &mut Window, - cx: &mut Context, - ) { - self.do_stage_or_unstage_and_next(true, window, cx); - } - - pub fn unstage_and_next( - &mut self, - _: &::git::UnstageAndNext, - window: &mut Window, - cx: &mut Context, - ) { - self.do_stage_or_unstage_and_next(false, window, cx); - } - - pub fn stage_or_unstage_diff_hunks( - &mut self, - stage: bool, - ranges: Vec>, - cx: &mut Context, - ) { - 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); - } - - fn save_buffers_for_ranges_if_needed( - &mut self, - ranges: &[Range], - cx: &mut Context, - ) -> Task> { - 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, - ) { - let ranges = self.selections.disjoint_anchor_ranges().collect::>(); - - 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::(&snapshot.display_snapshot) - .head(); - - self.go_to_hunk_before_or_after_position( - &snapshot, - position, - Direction::Next, - wrap_around, - window, - cx, - ); - } - - pub(crate) fn do_stage_or_unstage( - &self, - stage: bool, - buffer_id: BufferId, - hunks: impl Iterator, - 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::>(), - &buffer_snapshot, - file_exists, - cx, - ) - }); - None - } - - pub fn expand_selected_diff_hunks(&mut self, cx: &mut Context) { - 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 clear_expanded_diff_hunks(&mut self, cx: &mut Context) -> 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 - } - }) - } - - 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) - } - - fn toggle_diff_hunks_in_ranges( - &mut self, - ranges: Vec>, - cx: &mut Context, - ) { - 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); - }) - } - - fn toggle_single_diff_hunk(&mut self, range: Range, cx: &mut Context) { - self.buffer.update(cx, |buffer, cx| { - buffer.toggle_single_diff_hunk(range, cx); - }) - } - - pub(crate) fn apply_all_diff_hunks( - &mut self, - _: &ApplyAllDiffHunks, - window: &mut Window, - cx: &mut Context, - ) { - 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); - } + _: &ExpandAllDiffHunks, + _window: &mut Window, + cx: &mut Context, + ) { + self.buffer.update(cx, |buffer, cx| { + buffer.expand_diff_hunks(vec![Anchor::Min..Anchor::Max], cx) + }); } - pub(crate) fn apply_selected_diff_hunks( + pub fn collapse_all_diff_hunks( &mut self, - _: &ApplyDiffHunk, - window: &mut Window, + _: &CollapseAllDiffHunks, + _window: &mut Window, cx: &mut Context, ) { - 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); - }); - } + self.buffer.update(cx, |buffer, cx| { + buffer.collapse_diff_hunks(vec![Anchor::Min..Anchor::Max], 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 fn toggle_selected_diff_hunks( + &mut self, + _: &ToggleSelectedDiffHunks, + _window: &mut Window, + cx: &mut Context, + ) { + let ranges: Vec<_> = self + .selections + .disjoint_anchors() + .iter() + .map(|s| s.range()) + .collect(); + self.toggle_diff_hunks_in_ranges(ranges, cx); } pub fn set_gutter_hovered(&mut self, hovered: bool, cx: &mut Context) { @@ -19105,393 +17515,39 @@ impl Editor { Box::new(crate::actions::CancelEditReviewComment { id: comment_id, }), - cx, - ); - }), - ) - .child( - IconButton::new( - format!("diff-review-confirm-edit-{comment_id}"), - IconName::Return, - ) - .icon_color(ui::Color::Muted) - .icon_size(action_icon_size) - .tooltip(Tooltip::text("Confirm")) - .on_click(move |_, window, cx| { - window.dispatch_action( - Box::new(crate::actions::ConfirmEditReviewComment { - id: comment_id, - }), - cx, - ); - }), - ) - .into_any_element() - } else { - // Display mode: no action buttons for now (edit/delete not yet implemented) - gpui::Empty.into_any_element() - }) - } - - pub fn set_masked(&mut self, masked: bool, cx: &mut Context) { - if self.display_map.read(cx).masked != masked { - self.display_map.update(cx, |map, _| map.masked = masked); - } - cx.notify() - } - - pub fn working_directory(&self, cx: &App) -> Option { - 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 - } - - 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 fn target_file_abs_path(&self, cx: &mut Context) -> Option { - 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))) - } - }) - } - - pub fn reveal_in_finder( - &mut self, - _: &RevealInFileManager, - _window: &mut Window, - cx: &mut Context, - ) { - 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 fn copy_path( - &mut self, - _: &zed_actions::workspace::CopyPath, - _window: &mut Window, - cx: &mut Context, - ) { - 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 fn copy_relative_path( - &mut self, - _: &zed_actions::workspace::CopyRelativePath, - _window: &mut Window, - cx: &mut Context, - ) { - 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(); - } - } - - /// 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 { - if let Some(buffer) = self.buffer.read(cx).as_singleton() { - buffer.read(cx).project_path(cx) - } else { - None - } - } - - // Returns true if the editor handled a go-to-line request - pub fn go_to_active_debug_line(&mut self, window: &mut Window, cx: &mut Context) -> 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::(); - 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::(); - 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::(); - - self.go_to_line::( - multibuffer_anchor, - Some(cx.theme().colors().editor_debugger_active_line_background), - window, - cx, - ); - - cx.notify(); - - Some(()) - }) - .is_some() - } - - pub fn copy_file_name_without_extension( - &mut self, - _: &CopyFileNameWithoutExtension, - _: &mut Window, - cx: &mut Context, - ) { - 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) { - 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.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.toggle_git_blame_inline_internal(true, window, cx); - cx.notify(); - } - - pub fn open_git_blame_commit( - &mut self, - _: &OpenGitBlameCommit, - window: &mut Window, - cx: &mut Context, - ) { - self.open_git_blame_commit_internal(window, cx); - } - - fn open_git_blame_commit_internal( - &mut self, - window: &mut Window, - cx: &mut Context, - ) -> Option<()> { - let blame = self.blame.as_ref()?; - let snapshot = self.snapshot(window, cx); - let cursor = self - .selections - .newest::(&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::().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 - } - - pub fn git_blame_inline_enabled(&self) -> bool { - self.git_blame_inline_enabled - } - - pub fn toggle_selection_menu( - &mut self, - _: &ToggleSelectionMenu, - _: &mut Window, - cx: &mut Context, - ) { - 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 selection_menu_enabled(&self, cx: &App) -> bool { - self.show_selection_menu - .unwrap_or_else(|| EditorSettings::get_global(cx).toolbar.selections_menu) - } - - fn start_git_blame( - &mut self, - user_triggered: bool, - window: &mut Window, - cx: &mut Context, - ) { - 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); - } - } - - fn toggle_git_blame_inline_internal( - &mut self, - user_triggered: bool, - window: &mut Window, - cx: &mut Context, - ) { - 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(); + cx, + ); + }), + ) + .child( + IconButton::new( + format!("diff-review-confirm-edit-{comment_id}"), + IconName::Return, + ) + .icon_color(ui::Color::Muted) + .icon_size(action_icon_size) + .tooltip(Tooltip::text("Confirm")) + .on_click(move |_, window, cx| { + window.dispatch_action( + Box::new(crate::actions::ConfirmEditReviewComment { + id: comment_id, + }), + cx, + ); + }), + ) + .into_any_element() + } else { + // Display mode: no action buttons for now (edit/delete not yet implemented) + gpui::Empty.into_any_element() + }) } - fn start_git_blame_inline( - &mut self, - user_triggered: bool, - window: &mut Window, - cx: &mut Context, - ) { - 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 fn set_masked(&mut self, masked: bool, cx: &mut Context) { + if self.display_map.read(cx).masked != masked { + self.display_map.update(cx, |map, _| map.masked = masked); } - } - - pub fn blame(&self) -> Option<&Entity> { - self.blame.as_ref() - } - - pub fn show_git_blame_gutter(&self) -> bool { - self.show_git_blame_gutter - } - - pub fn render_git_blame_gutter(&self, cx: &App) -> bool { - !self.mode().is_minimap() && self.show_git_blame_gutter && self.has_blame_entries(cx) - } - - pub 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_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 + cx.notify() } fn get_permalink_to_line(&self, cx: &mut Context) -> Task> { @@ -21118,19 +19174,6 @@ impl Editor { }); } - fn marked_text_ranges(&self, cx: &App) -> Option>> { - 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(), - ) - } - fn selection_replacement_ranges( &self, range: Range, @@ -21324,46 +19367,6 @@ impl Editor { mouse_context_menu::deploy_context_menu(self, None, position, window, cx); } - pub fn replay_insert_event( - &mut self, - text: &str, - relative_utf16_range: Option>, - window: &mut Window, - cx: &mut Context, - ) { - 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::(&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 is_focused(&self, window: &Window) -> bool { self.focus_handle.is_focused(window) } @@ -21456,90 +19459,6 @@ impl Editor { cx.notify(); } - pub fn observe_pending_input(&mut self, window: &mut Window, cx: &mut Context) { - 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::(&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::(&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 fn register_action_renderer( &mut self, listener: impl Fn(&Editor, &mut Window, &mut Context) + 'static, @@ -22198,532 +20117,54 @@ fn process_completion_for_edit( { let range_after_cursor = *cursor_position..replace_range.end; let text_after_cursor = buffer - .text_for_range( - buffer.anchor_before(range_after_cursor.start) - ..buffer.anchor_after(range_after_cursor.end), - ) - .collect::() - .to_ascii_lowercase(); - completion - .label - .text - .to_ascii_lowercase() - .ends_with(&text_after_cursor) - } else { - true - } - } - } - } - }; - - if should_replace { - replace_range.clone() - } else { - insert_range.clone() - } - } else { - replace_range.clone() - } - }; - - if range_to_replace - .end - .cmp(cursor_position, &buffer_snapshot) - .is_lt() - { - range_to_replace.end = *cursor_position; - } - - CompletionEdit { - new_text, - replace_range: range_to_replace, - snippet, - } -} - -struct CompletionEdit { - new_text: String, - replace_range: Range, - snippet: Option, -} - -fn comment_delimiter_for_newline( - start_point: &Point, - buffer: &MultiBufferSnapshot, - language: &LanguageScope, -) -> Option> { - 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::(); - 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::(); - - 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> { - 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::(); - 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::(); - 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 - } -} - -const ORDERED_LIST_MAX_MARKER_LEN: usize = 16; - -fn list_delimiter_for_newline( - start_point: &Point, - buffer: &MultiBufferSnapshot, - language: &LanguageScope, - newline_config: &mut NewlineConfig, -) -> Option> { - 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(), - }; + .text_for_range( + buffer.anchor_before(range_after_cursor.start) + ..buffer.anchor_after(range_after_cursor.end), + ) + .collect::() + .to_ascii_lowercase(); + completion + .label + .text + .to_ascii_lowercase() + .ends_with(&text_after_cursor) + } else { + true + } + } + } } - } - - 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(), - }; - } + if should_replace { + replace_range.clone() + } else { + insert_range.clone() } - - return None; + } else { + replace_range.clone() } - } - - None -} - -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::>() - }) - .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; - } + if range_to_replace + .end + .cmp(cursor_position, &buffer_snapshot) + .is_lt() + { + range_to_replace.end = *cursor_position; } - 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(); - } + CompletionEdit { + new_text, + replace_range: range_to_replace, + snippet, } - - 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, - prevent_auto_indent: bool, - }, - /// Clear the current line - ClearCurrentLine, - /// Unindent the current line and add continuation - UnindentCurrentLine { continuation: Arc }, } -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, - 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::(); - let trailing_whitespace_len = buffer - .chars_at(range.end) - .take_while(|c| c.is_whitespace() && *c != '\n') - .map(|c| c.len_utf8()) - .sum::(); - 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, - ) -> 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> = 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') - } +struct CompletionEdit { + new_text: String, + replace_range: Range, + snippet: Option, } fn update_uncommitted_diff_for_buffer( @@ -23524,353 +20965,6 @@ impl Render for Editor { } } -impl EntityInputHandler for Editor { - fn text_for_range( - &mut self, - range_utf16: Range, - adjusted_range: &mut Option>, - _: &mut Window, - cx: &mut Context, - ) -> Option { - 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, - ) -> Option { - // 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::(&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) -> Option> { - 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.clear_highlights(HighlightKey::InputComposition, cx); - self.ime_transaction.take(); - } - - fn replace_text_in_range( - &mut self, - range_utf16: Option>, - text: &str, - window: &mut Window, - cx: &mut Context, - ) { - 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::(&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>, - text: &str, - new_selected_range_utf16: Option>, - window: &mut Window, - cx: &mut Context, - ) { - 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::(&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::>() - }; - - 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::>(); - - 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, - element_bounds: gpui::Bounds, - window: &mut Window, - cx: &mut Context, - ) -> Option> { - 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, - _window: &mut Window, - _cx: &mut Context, - ) -> Option { - 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) -> bool { - self.expects_character_input - } -} - trait SelectionExt { fn display_range(&self, map: &DisplaySnapshot) -> Range; fn spanned_rows( diff --git a/crates/editor/src/git.rs b/crates/editor/src/git.rs index 080babe4c682a5fbc665447678862477c9156da2..dd4b156dcab8640cfee4ff1593bbf34425b8496f 100644 --- a/crates/editor/src/git.rs +++ b/crates/editor/src/git.rs @@ -1 +1,710 @@ pub mod blame; + +use super::*; + +impl Editor { + pub fn diff_hunks_in_ranges<'a>( + &'a self, + ranges: &'a [Range], + buffer: &'a MultiBufferSnapshot, + ) -> impl 'a + Iterator { + 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.render_diff_hunk_controls = render_diff_hunk_controls; + cx.notify(); + } + + pub fn working_directory(&self, cx: &App) -> Option { + 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) -> Option { + 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 { + 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.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> { + 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) { + 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, + ) { + 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) { + 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.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.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, + ) { + 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.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.do_stage_or_unstage_and_next(false, window, cx); + } + + pub(super) fn stage_or_unstage_diff_hunks( + &mut self, + stage: bool, + ranges: Vec>, + cx: &mut Context, + ) { + 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, + 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::>(), + &buffer_snapshot, + file_exists, + cx, + ) + }); + None + } + + pub(super) fn clear_expanded_diff_hunks(&mut self, cx: &mut Context) -> 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>, + cx: &mut Context, + ) { + 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, cx: &mut Context) { + 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, + ) { + 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, + ) { + 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, + ) { + 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, + ) { + 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, + ) { + 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, + ) -> 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::(); + 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::(); + 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::(); + + self.go_to_line::( + 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.open_git_blame_commit_internal(window, cx); + } + + pub(super) fn start_git_blame( + &mut self, + user_triggered: bool, + window: &mut Window, + cx: &mut Context, + ) { + 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, + ) { + 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.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], + 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], + cx: &mut Context, + ) -> Task> { + 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, + ) { + let ranges = self.selections.disjoint_anchor_ranges().collect::>(); + + 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::(&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, + ) -> Option<()> { + let blame = self.blame.as_ref()?; + let snapshot = self.snapshot(window, cx); + let cursor = self + .selections + .newest::(&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::().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 + } +} diff --git a/crates/editor/src/input.rs b/crates/editor/src/input.rs new file mode 100644 index 0000000000000000000000000000000000000000..1634ad3556401f6ca2a9bce7f316c431298830ca --- /dev/null +++ b/crates/editor/src/input.rs @@ -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>, + window: &mut Window, + cx: &mut Context, + ) { + 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::(&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) { + let text: Arc = 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::( + 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::>(); + + 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 = 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) { + 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::(&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) { + 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) { + if self.read_only(cx) { + return; + } + + let mut buffer_edits: HashMap, Vec)> = 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) { + 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, 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.replace_selections("", None, window, cx, true); + } + + pub(super) fn observe_pending_input(&mut self, window: &mut Window, cx: &mut Context) { + 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::(&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::(&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, + cx: &App, + ) -> Option, Vec>>> { + 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>> { + 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, + window: &mut Window, + cx: &mut Context, + apply_linked_edits: bool, + ) { + if self.read_only(cx) { + return; + } + + let text: Arc = 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::>() + }; + 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) { + let selections = self + .selections + .all::(&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], + 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 { + 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::(); + 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>, + buffer: &'a MultiBufferSnapshot, + ) -> impl Iterator, 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, Vec>)>, + cx: &mut Context, + ) -> 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::>() + }) + .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, + prevent_auto_indent: bool, + }, + /// Clear the current line + ClearCurrentLine, + /// Unindent the current line and add continuation + UnindentCurrentLine { continuation: Arc }, +} + +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, + 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::(); + let trailing_whitespace_len = buffer + .chars_at(range.end) + .take_while(|c| c.is_whitespace() && *c != '\n') + .map(|c| c.len_utf8()) + .sum::(); + 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, + ) -> 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> = 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> { + 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::(); + 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::(); + + 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> { + 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::(); + 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::(); + 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> { + 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, + adjusted_range: &mut Option>, + _: &mut Window, + cx: &mut Context, + ) -> Option { + 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, + ) -> Option { + // 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::(&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) -> Option> { + 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.clear_highlights(HighlightKey::InputComposition, cx); + self.ime_transaction.take(); + } + + fn replace_text_in_range( + &mut self, + range_utf16: Option>, + text: &str, + window: &mut Window, + cx: &mut Context, + ) { + 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::(&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>, + text: &str, + new_selected_range_utf16: Option>, + window: &mut Window, + cx: &mut Context, + ) { + 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::(&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::>() + }; + + 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::>(); + + 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, + element_bounds: gpui::Bounds, + window: &mut Window, + cx: &mut Context, + ) -> Option> { + 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, + _window: &mut Window, + _cx: &mut Context, + ) -> Option { + 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) -> bool { + self.expects_character_input + } +}