Detailed changes
@@ -22,6 +22,7 @@ mod document_colors;
mod document_symbols;
mod editor_settings;
mod element;
+mod fold;
mod folding_ranges;
mod git;
mod highlight_matching_bracket;
@@ -62,6 +63,7 @@ mod completions;
mod config;
mod diagnostics;
mod rewrap;
+mod selection;
pub(crate) use actions::*;
pub use code_actions::CodeActionProvider;
@@ -1444,13 +1446,6 @@ impl GutterDimensions {
pub fn full_width(&self) -> Pixels {
self.margin + self.width
}
-
- /// The width of the space reserved for the fold indicators,
- /// use alongside 'justify_end' and `gutter_width` to
- /// right align content with the line numbers
- pub fn fold_area_width(&self) -> Pixels {
- self.margin + self.right_padding
- }
}
struct CharacterDimensions {
@@ -2784,30 +2779,6 @@ impl Editor {
.is_some_and(|menu| menu.context_menu.focus_handle(cx).is_focused(window))
}
- pub fn is_range_selected(&mut self, range: &Range<Anchor>, cx: &mut Context<Self>) -> bool {
- if self
- .selections
- .pending_anchor()
- .is_some_and(|pending_selection| {
- let snapshot = self.buffer().read(cx).snapshot(cx);
- pending_selection.range().includes(range, &snapshot)
- })
- {
- return true;
- }
-
- self.selections
- .disjoint_in_range::<MultiBufferOffset>(range.clone(), &self.display_snapshot(cx))
- .into_iter()
- .any(|selection| {
- // This is needed to cover a corner case, if we just check for an existing
- // selection in the fold range, having a cursor at the start of the fold
- // marks it as selected. Non-empty selections don't cause this.
- let length = selection.end - selection.start;
- length > 0
- })
- }
-
pub fn key_context(&self, window: &mut Window, cx: &mut App) -> KeyContext {
self.key_context_internal(self.has_active_edit_prediction(), window, cx)
}
@@ -3621,449 +3592,6 @@ impl Editor {
self.use_modal_editing
}
- fn selections_did_change(
- &mut self,
- local: bool,
- old_cursor_position: &Anchor,
- effects: SelectionEffects,
- window: &mut Window,
- cx: &mut Context<Self>,
- ) {
- self.last_selection_from_search = effects.from_search;
- window.invalidate_character_coordinates();
-
- // Copy selections to primary selection buffer
- #[cfg(any(target_os = "linux", target_os = "freebsd"))]
- if local {
- let selections = self
- .selections
- .all::<MultiBufferOffset>(&self.display_snapshot(cx));
- let buffer_handle = self.buffer.read(cx).read(cx);
-
- let mut text = String::new();
- for (index, selection) in selections.iter().enumerate() {
- let text_for_selection = buffer_handle
- .text_for_range(selection.start..selection.end)
- .collect::<String>();
-
- text.push_str(&text_for_selection);
- if index != selections.len() - 1 {
- text.push('\n');
- }
- }
-
- if !text.is_empty() {
- cx.write_to_primary(ClipboardItem::new_string(text));
- }
- }
-
- let selection_anchors = self.selections.disjoint_anchors_arc();
-
- if self.focus_handle.is_focused(window) && self.leader_id.is_none() {
- self.buffer.update(cx, |buffer, cx| {
- buffer.set_active_selections(
- &selection_anchors,
- self.selections.line_mode(),
- self.cursor_shape,
- cx,
- )
- });
- }
- let display_map = self
- .display_map
- .update(cx, |display_map, cx| display_map.snapshot(cx));
- let buffer = display_map.buffer_snapshot();
- if self.selections.count() == 1 {
- self.add_selections_state = None;
- }
- self.select_next_state = None;
- self.select_prev_state = None;
- self.select_syntax_node_history.try_clear();
- self.invalidate_autoclose_regions(&selection_anchors, buffer);
- self.snippet_stack.invalidate(&selection_anchors, buffer);
- self.take_rename(false, window, cx);
-
- let newest_selection = self.selections.newest_anchor();
- let new_cursor_position = newest_selection.head();
- let selection_start = newest_selection.start;
-
- if effects.nav_history.is_none() || effects.nav_history == Some(true) {
- self.push_to_nav_history(
- *old_cursor_position,
- Some(new_cursor_position.to_point(buffer)),
- false,
- effects.nav_history == Some(true),
- cx,
- );
- }
-
- if local {
- if let Some((anchor, _)) = buffer.anchor_to_buffer_anchor(new_cursor_position) {
- self.register_buffer(anchor.buffer_id, cx);
- }
-
- let mut context_menu = self.context_menu.borrow_mut();
- let completion_menu = match context_menu.as_ref() {
- Some(CodeContextMenu::Completions(menu)) => Some(menu),
- Some(CodeContextMenu::CodeActions(_)) => {
- *context_menu = None;
- None
- }
- None => None,
- };
- let completion_position = completion_menu.map(|menu| menu.initial_position);
- drop(context_menu);
-
- if effects.completions
- && let Some(completion_position) = completion_position
- {
- let start_offset = selection_start.to_offset(buffer);
- let position_matches = start_offset == completion_position.to_offset(buffer);
- let continue_showing = if let Some((snap, ..)) =
- buffer.point_to_buffer_offset(completion_position)
- && !snap.capability.editable()
- {
- false
- } else if position_matches {
- if self.snippet_stack.is_empty() {
- buffer.char_kind_before(start_offset, Some(CharScopeContext::Completion))
- == Some(CharKind::Word)
- } else {
- // Snippet choices can be shown even when the cursor is in whitespace.
- // Dismissing the menu with actions like backspace is handled by
- // invalidation regions.
- true
- }
- } else {
- false
- };
-
- if continue_showing {
- self.open_or_update_completions_menu(None, None, false, window, cx);
- } else {
- self.hide_context_menu(window, cx);
- }
- }
-
- hide_hover(self, cx);
-
- self.refresh_code_actions_for_selection(window, cx);
- self.refresh_document_highlights(cx);
- refresh_linked_ranges(self, window, cx);
-
- self.refresh_selected_text_highlights(&display_map, false, window, cx);
- self.refresh_matching_bracket_highlights(&display_map, cx);
- self.refresh_outline_symbols_at_cursor(cx);
- self.update_visible_edit_prediction(window, cx);
- self.hide_blame_popover(true, cx);
- if self.git_blame_inline_enabled {
- self.start_inline_blame_timer(window, cx);
- }
- }
-
- self.blink_manager.update(cx, BlinkManager::pause_blinking);
-
- if local && !self.suppress_selection_callback {
- if let Some(callback) = self.on_local_selections_changed.as_ref() {
- let cursor_position = self.selections.newest::<Point>(&display_map).head();
- callback(cursor_position, window, cx);
- }
- }
-
- cx.emit(EditorEvent::SelectionsChanged { local });
-
- let selections = &self.selections.disjoint_anchors_arc();
- if local && let Some(buffer_snapshot) = buffer.as_singleton() {
- let inmemory_selections = selections
- .iter()
- .map(|s| {
- let start = s.range().start.text_anchor_in(buffer_snapshot);
- let end = s.range().end.text_anchor_in(buffer_snapshot);
- (start..end).to_point(buffer_snapshot)
- })
- .collect();
- self.update_restoration_data(cx, |data| {
- data.selections = inmemory_selections;
- });
-
- if WorkspaceSettings::get(None, cx).restore_on_startup
- != RestoreOnStartupBehavior::EmptyTab
- && let Some(workspace_id) = self.workspace_serialization_id(cx)
- {
- let snapshot = self.buffer().read(cx).snapshot(cx);
- let selections = selections.clone();
- let background_executor = cx.background_executor().clone();
- let editor_id = cx.entity().entity_id().as_u64() as ItemId;
- let db = EditorDb::global(cx);
- self.serialize_selections = cx.background_spawn(async move {
- background_executor.timer(SERIALIZATION_THROTTLE_TIME).await;
- let db_selections = selections
- .iter()
- .map(|selection| {
- (
- selection.start.to_offset(&snapshot).0,
- selection.end.to_offset(&snapshot).0,
- )
- })
- .collect();
-
- db.save_editor_selections(editor_id, workspace_id, db_selections)
- .await
- .with_context(|| {
- format!(
- "persisting editor selections for editor {editor_id}, \
- workspace {workspace_id:?}"
- )
- })
- .log_err();
- });
- }
- }
-
- cx.notify();
- }
-
- fn folds_did_change(&mut self, cx: &mut Context<Self>) {
- use text::ToOffset as _;
-
- if self.mode.is_minimap()
- || WorkspaceSettings::get(None, cx).restore_on_startup
- == RestoreOnStartupBehavior::EmptyTab
- {
- return;
- }
-
- let display_snapshot = self
- .display_map
- .update(cx, |display_map, cx| display_map.snapshot(cx));
- let Some(buffer_snapshot) = display_snapshot.buffer_snapshot().as_singleton() else {
- return;
- };
- let inmemory_folds = display_snapshot
- .folds_in_range(MultiBufferOffset(0)..display_snapshot.buffer_snapshot().len())
- .map(|fold| {
- let start = fold.range.start.text_anchor_in(buffer_snapshot);
- let end = fold.range.end.text_anchor_in(buffer_snapshot);
- (start..end).to_point(buffer_snapshot)
- })
- .collect();
- self.update_restoration_data(cx, |data| {
- data.folds = inmemory_folds;
- });
-
- let Some(workspace_id) = self.workspace_serialization_id(cx) else {
- return;
- };
-
- // Get file path for path-based fold storage (survives tab close)
- let Some(file_path) = self.buffer().read(cx).as_singleton().and_then(|buffer| {
- project::File::from_dyn(buffer.read(cx).file())
- .map(|file| Arc::<Path>::from(file.abs_path(cx)))
- }) else {
- return;
- };
-
- let background_executor = cx.background_executor().clone();
- const FINGERPRINT_LEN: usize = 32;
- let db_folds = display_snapshot
- .folds_in_range(MultiBufferOffset(0)..display_snapshot.buffer_snapshot().len())
- .map(|fold| {
- let start = fold
- .range
- .start
- .text_anchor_in(buffer_snapshot)
- .to_offset(buffer_snapshot);
- let end = fold
- .range
- .end
- .text_anchor_in(buffer_snapshot)
- .to_offset(buffer_snapshot);
-
- // Extract fingerprints - content at fold boundaries for validation on restore
- // Both fingerprints must be INSIDE the fold to avoid capturing surrounding
- // content that might change independently.
- // start_fp: first min(32, fold_len) bytes of fold content
- // end_fp: last min(32, fold_len) bytes of fold content
- // Clip to character boundaries to handle multibyte UTF-8 characters.
- let fold_len = end - start;
- let start_fp_end = buffer_snapshot
- .clip_offset(start + std::cmp::min(FINGERPRINT_LEN, fold_len), Bias::Left);
- let start_fp: String = buffer_snapshot
- .text_for_range(start..start_fp_end)
- .collect();
- let end_fp_start = buffer_snapshot
- .clip_offset(end.saturating_sub(FINGERPRINT_LEN).max(start), Bias::Right);
- let end_fp: String = buffer_snapshot.text_for_range(end_fp_start..end).collect();
-
- (start, end, start_fp, end_fp)
- })
- .collect::<Vec<_>>();
- let db = EditorDb::global(cx);
- self.serialize_folds = cx.background_spawn(async move {
- background_executor.timer(SERIALIZATION_THROTTLE_TIME).await;
- if db_folds.is_empty() {
- // No folds - delete any persisted folds for this file
- db.delete_file_folds(workspace_id, file_path)
- .await
- .with_context(|| format!("deleting file folds for workspace {workspace_id:?}"))
- .log_err();
- } else {
- db.save_file_folds(workspace_id, file_path, db_folds)
- .await
- .with_context(|| {
- format!("persisting file folds for workspace {workspace_id:?}")
- })
- .log_err();
- }
- });
- }
-
- pub fn sync_selections(
- &mut self,
- other: Entity<Editor>,
- cx: &mut Context<Self>,
- ) -> gpui::Subscription {
- let other_selections = other.read(cx).selections.disjoint_anchors().to_vec();
- if !other_selections.is_empty() {
- self.selections
- .change_with(&self.display_snapshot(cx), |selections| {
- selections.select_anchors(other_selections);
- });
- }
-
- let other_subscription = cx.subscribe(&other, |this, other, other_evt, cx| {
- if let EditorEvent::SelectionsChanged { local: true } = other_evt {
- let other_selections = other.read(cx).selections.disjoint_anchors().to_vec();
- if other_selections.is_empty() {
- return;
- }
- let snapshot = this.display_snapshot(cx);
- this.selections.change_with(&snapshot, |selections| {
- selections.select_anchors(other_selections);
- });
- }
- });
-
- let this_subscription = cx.subscribe_self::<EditorEvent>(move |this, this_evt, cx| {
- if let EditorEvent::SelectionsChanged { local: true } = this_evt {
- let these_selections = this.selections.disjoint_anchors().to_vec();
- if these_selections.is_empty() {
- return;
- }
- other.update(cx, |other_editor, cx| {
- let snapshot = other_editor.display_snapshot(cx);
- other_editor
- .selections
- .change_with(&snapshot, |selections| {
- selections.select_anchors(these_selections);
- })
- });
- }
- });
-
- Subscription::join(other_subscription, this_subscription)
- }
-
- fn unfold_buffers_with_selections(&mut self, cx: &mut Context<Self>) {
- if self.buffer().read(cx).is_singleton() {
- return;
- }
- let snapshot = self.buffer.read(cx).snapshot(cx);
- let buffer_ids: HashSet<BufferId> = self
- .selections
- .disjoint_anchor_ranges()
- .flat_map(|range| snapshot.buffer_ids_for_range(range))
- .collect();
- for buffer_id in buffer_ids {
- self.unfold_buffer(buffer_id, cx);
- }
- }
-
- /// Changes selections using the provided mutation function. Changes to `self.selections` occur
- /// immediately, but when run within `transact` or `with_selection_effects_deferred` other
- /// effects of selection change occur at the end of the transaction.
- pub fn change_selections<R>(
- &mut self,
- effects: SelectionEffects,
- window: &mut Window,
- cx: &mut Context<Self>,
- change: impl FnOnce(&mut MutableSelectionsCollection<'_, '_>) -> R,
- ) -> R {
- let snapshot = self.display_snapshot(cx);
- if let Some(state) = &mut self.deferred_selection_effects_state {
- state.effects.scroll = effects.scroll.or(state.effects.scroll);
- state.effects.completions = effects.completions;
- state.effects.nav_history = effects.nav_history.or(state.effects.nav_history);
- let (changed, result) = self.selections.change_with(&snapshot, change);
- state.changed |= changed;
- return result;
- }
- let mut state = DeferredSelectionEffectsState {
- changed: false,
- effects,
- old_cursor_position: self.selections.newest_anchor().head(),
- history_entry: SelectionHistoryEntry {
- selections: self.selections.disjoint_anchors_arc(),
- select_next_state: self.select_next_state.clone(),
- select_prev_state: self.select_prev_state.clone(),
- add_selections_state: self.add_selections_state.clone(),
- },
- };
- let (changed, result) = self.selections.change_with(&snapshot, change);
- state.changed = state.changed || changed;
- if self.defer_selection_effects {
- self.deferred_selection_effects_state = Some(state);
- } else {
- self.apply_selection_effects(state, window, cx);
- }
- result
- }
-
- /// Defers the effects of selection change, so that the effects of multiple calls to
- /// `change_selections` are applied at the end. This way these intermediate states aren't added
- /// to selection history and the state of popovers based on selection position aren't
- /// erroneously updated.
- pub fn with_selection_effects_deferred<R>(
- &mut self,
- window: &mut Window,
- cx: &mut Context<Self>,
- update: impl FnOnce(&mut Self, &mut Window, &mut Context<Self>) -> R,
- ) -> R {
- let already_deferred = self.defer_selection_effects;
- self.defer_selection_effects = true;
- let result = update(self, window, cx);
- if !already_deferred {
- self.defer_selection_effects = false;
- if let Some(state) = self.deferred_selection_effects_state.take() {
- self.apply_selection_effects(state, window, cx);
- }
- }
- result
- }
-
- fn apply_selection_effects(
- &mut self,
- state: DeferredSelectionEffectsState,
- window: &mut Window,
- cx: &mut Context<Self>,
- ) {
- if state.changed {
- self.selection_history.push(state.history_entry);
-
- if let Some(autoscroll) = state.effects.scroll {
- self.request_autoscroll(autoscroll, cx);
- }
-
- let old_cursor_position = &state.old_cursor_position;
-
- self.selections_did_change(true, old_cursor_position, state.effects, window, cx);
-
- if self.should_open_signature_help_automatically(old_cursor_position, cx) {
- self.show_signature_help_auto(window, cx);
- }
- }
- }
-
pub fn edit<I, S, T>(&mut self, edits: I, cx: &mut Context<Self>)
where
I: IntoIterator<Item = (Range<S>, T)>,
@@ -4118,515 +3646,41 @@ impl Editor {
});
}
- fn select(&mut self, phase: SelectPhase, window: &mut Window, cx: &mut Context<Self>) {
- self.hide_context_menu(window, cx);
+ pub fn cancel(&mut self, _: &Cancel, window: &mut Window, cx: &mut Context<Self>) {
+ self.selection_mark_mode = false;
+ self.selection_drag_state = SelectionDragState::None;
- match phase {
- SelectPhase::Begin {
- position,
- add,
- click_count,
- } => self.begin_selection(position, add, click_count, window, cx),
- SelectPhase::BeginColumnar {
- position,
- goal_column,
- reset,
- mode,
- } => self.begin_columnar_selection(position, goal_column, reset, mode, window, cx),
- SelectPhase::Extend {
- position,
- click_count,
- } => self.extend_selection(position, click_count, window, cx),
- SelectPhase::Update {
- position,
- goal_column,
- scroll_delta,
- } => self.update_selection(position, goal_column, scroll_delta, window, cx),
- SelectPhase::End => self.end_selection(window, cx),
+ if self.dismiss_menus_and_popups(true, window, cx) {
+ cx.notify();
+ return;
}
- }
-
- fn extend_selection(
- &mut self,
- position: DisplayPoint,
- click_count: usize,
- window: &mut Window,
- cx: &mut Context<Self>,
- ) {
- let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
- let tail = self
- .selections
- .newest::<MultiBufferOffset>(&display_map)
- .tail();
- let click_count = click_count.max(match self.selections.select_mode() {
- SelectMode::Character => 1,
- SelectMode::Word(_) => 2,
- SelectMode::Line(_) => 3,
- SelectMode::All => 4,
- });
- self.begin_selection(position, false, click_count, window, cx);
-
- let tail_anchor = display_map.buffer_snapshot().anchor_before(tail);
-
- let current_selection = match self.selections.select_mode() {
- SelectMode::Character | SelectMode::All => tail_anchor..tail_anchor,
- SelectMode::Word(range) | SelectMode::Line(range) => range.clone(),
- };
-
- let mut pending_selection = self
- .selections
- .pending_anchor()
- .cloned()
- .expect("extend_selection not called with pending selection");
-
- if pending_selection
- .start
- .cmp(¤t_selection.start, display_map.buffer_snapshot())
- == Ordering::Greater
- {
- pending_selection.start = current_selection.start;
+ if self.clear_expanded_diff_hunks(cx) {
+ cx.notify();
+ return;
}
- if pending_selection
- .end
- .cmp(¤t_selection.end, display_map.buffer_snapshot())
- == Ordering::Less
- {
- pending_selection.end = current_selection.end;
- pending_selection.reversed = true;
+ if self.show_git_blame_gutter {
+ self.show_git_blame_gutter = false;
+ cx.notify();
+ return;
}
- let mut pending_mode = self.selections.pending_mode().unwrap();
- match &mut pending_mode {
- SelectMode::Word(range) | SelectMode::Line(range) => *range = current_selection,
- _ => {}
+ if self.mode.is_full()
+ && self.change_selections(Default::default(), window, cx, |s| s.try_cancel())
+ {
+ cx.notify();
+ return;
}
- let effects = if EditorSettings::get_global(cx).autoscroll_on_clicks {
- SelectionEffects::scroll(Autoscroll::fit())
- } else {
- SelectionEffects::no_scroll()
- };
-
- self.change_selections(effects, window, cx, |s| {
- s.set_pending(pending_selection.clone(), pending_mode);
- s.set_is_extending(true);
- });
+ cx.propagate();
}
- fn begin_selection(
+ pub fn dismiss_menus_and_popups(
&mut self,
- position: DisplayPoint,
- add: bool,
- click_count: usize,
+ is_user_requested: bool,
window: &mut Window,
cx: &mut Context<Self>,
- ) {
- if !self.focus_handle.is_focused(window) {
- self.last_focused_descendant = None;
- window.focus(&self.focus_handle, cx);
- }
-
- let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
- let buffer = display_map.buffer_snapshot();
- let position = display_map.clip_point(position, Bias::Left);
-
- let start;
- let end;
- let mode;
- let mut auto_scroll;
- match click_count {
- 1 => {
- start = buffer.anchor_before(position.to_point(&display_map));
- end = start;
- mode = SelectMode::Character;
- auto_scroll = true;
- }
- 2 => {
- let position = display_map
- .clip_point(position, Bias::Left)
- .to_offset(&display_map, Bias::Left);
- let (range, _) = buffer.surrounding_word(position, None);
- start = buffer.anchor_before(range.start);
- end = buffer.anchor_before(range.end);
- mode = SelectMode::Word(start..end);
- auto_scroll = true;
- }
- 3 => {
- let position = display_map
- .clip_point(position, Bias::Left)
- .to_point(&display_map);
- let line_start = display_map.prev_line_boundary(position).0;
- let next_line_start = buffer.clip_point(
- display_map.next_line_boundary(position).0 + Point::new(1, 0),
- Bias::Left,
- );
- start = buffer.anchor_before(line_start);
- end = buffer.anchor_before(next_line_start);
- mode = SelectMode::Line(start..end);
- auto_scroll = true;
- }
- _ => {
- start = buffer.anchor_before(MultiBufferOffset(0));
- end = buffer.anchor_before(buffer.len());
- mode = SelectMode::All;
- auto_scroll = false;
- }
- }
- auto_scroll &= EditorSettings::get_global(cx).autoscroll_on_clicks;
-
- let point_to_delete: Option<usize> = {
- let selected_points: Vec<Selection<Point>> =
- self.selections.disjoint_in_range(start..end, &display_map);
-
- if !add || click_count > 1 {
- None
- } else if !selected_points.is_empty() {
- Some(selected_points[0].id)
- } else {
- let clicked_point_already_selected =
- self.selections.disjoint_anchors().iter().find(|selection| {
- selection.start.to_point(buffer) == start.to_point(buffer)
- || selection.end.to_point(buffer) == end.to_point(buffer)
- });
-
- clicked_point_already_selected.map(|selection| selection.id)
- }
- };
-
- let selections_count = self.selections.count();
- let effects = if auto_scroll {
- SelectionEffects::default()
- } else {
- SelectionEffects::no_scroll()
- };
-
- self.change_selections(effects, window, cx, |s| {
- if let Some(point_to_delete) = point_to_delete {
- s.delete(point_to_delete);
-
- if selections_count == 1 {
- s.set_pending_anchor_range(start..end, mode);
- }
- } else {
- if !add {
- s.clear_disjoint();
- }
-
- s.set_pending_anchor_range(start..end, mode);
- }
- });
- }
-
- fn begin_columnar_selection(
- &mut self,
- position: DisplayPoint,
- goal_column: u32,
- reset: bool,
- mode: ColumnarMode,
- window: &mut Window,
- cx: &mut Context<Self>,
- ) {
- if !self.focus_handle.is_focused(window) {
- self.last_focused_descendant = None;
- window.focus(&self.focus_handle, cx);
- }
-
- let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
-
- if reset {
- let pointer_position = display_map
- .buffer_snapshot()
- .anchor_before(position.to_point(&display_map));
-
- self.change_selections(
- SelectionEffects::scroll(Autoscroll::newest()),
- window,
- cx,
- |s| {
- s.clear_disjoint();
- s.set_pending_anchor_range(
- pointer_position..pointer_position,
- SelectMode::Character,
- );
- },
- );
- };
-
- let tail = self.selections.newest::<Point>(&display_map).tail();
- let selection_anchor = display_map.buffer_snapshot().anchor_before(tail);
- self.columnar_selection_state = match mode {
- ColumnarMode::FromMouse => Some(ColumnarSelectionState::FromMouse {
- selection_tail: selection_anchor,
- display_point: if reset {
- if position.column() != goal_column {
- Some(DisplayPoint::new(position.row(), goal_column))
- } else {
- None
- }
- } else {
- None
- },
- }),
- ColumnarMode::FromSelection => Some(ColumnarSelectionState::FromSelection {
- selection_tail: selection_anchor,
- }),
- };
-
- if !reset {
- self.select_columns(position, goal_column, &display_map, window, cx);
- }
- }
-
- fn update_selection(
- &mut self,
- position: DisplayPoint,
- goal_column: u32,
- scroll_delta: gpui::Point<f32>,
- window: &mut Window,
- cx: &mut Context<Self>,
- ) {
- let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
-
- if self.columnar_selection_state.is_some() {
- self.select_columns(position, goal_column, &display_map, window, cx);
- } else if let Some(mut pending) = self.selections.pending_anchor().cloned() {
- let buffer = display_map.buffer_snapshot();
- let head;
- let tail;
- let mode = self.selections.pending_mode().unwrap();
- match &mode {
- SelectMode::Character => {
- head = position.to_point(&display_map);
- tail = pending.tail().to_point(buffer);
- }
- SelectMode::Word(original_range) => {
- let offset = display_map
- .clip_point(position, Bias::Left)
- .to_offset(&display_map, Bias::Left);
- let original_range = original_range.to_offset(buffer);
-
- let head_offset = if buffer.is_inside_word(offset, None)
- || original_range.contains(&offset)
- {
- let (word_range, _) = buffer.surrounding_word(offset, None);
- if word_range.start < original_range.start {
- word_range.start
- } else {
- word_range.end
- }
- } else {
- offset
- };
-
- head = head_offset.to_point(buffer);
- if head_offset <= original_range.start {
- tail = original_range.end.to_point(buffer);
- } else {
- tail = original_range.start.to_point(buffer);
- }
- }
- SelectMode::Line(original_range) => {
- let original_range = original_range.to_point(display_map.buffer_snapshot());
-
- let position = display_map
- .clip_point(position, Bias::Left)
- .to_point(&display_map);
- let line_start = display_map.prev_line_boundary(position).0;
- let next_line_start = buffer.clip_point(
- display_map.next_line_boundary(position).0 + Point::new(1, 0),
- Bias::Left,
- );
-
- if line_start < original_range.start {
- head = line_start
- } else {
- head = next_line_start
- }
-
- if head <= original_range.start {
- tail = original_range.end;
- } else {
- tail = original_range.start;
- }
- }
- SelectMode::All => {
- return;
- }
- };
-
- if head < tail {
- pending.start = buffer.anchor_before(head);
- pending.end = buffer.anchor_before(tail);
- pending.reversed = true;
- } else {
- pending.start = buffer.anchor_before(tail);
- pending.end = buffer.anchor_before(head);
- pending.reversed = false;
- }
-
- self.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
- s.set_pending(pending.clone(), mode);
- });
- } else {
- log::error!("update_selection dispatched with no pending selection");
- return;
- }
-
- self.apply_scroll_delta(scroll_delta, window, cx);
- cx.notify();
- }
-
- fn end_selection(&mut self, window: &mut Window, cx: &mut Context<Self>) {
- self.columnar_selection_state.take();
- if let Some(pending_mode) = self.selections.pending_mode() {
- let selections = self
- .selections
- .all::<MultiBufferOffset>(&self.display_snapshot(cx));
- self.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
- s.select(selections);
- s.clear_pending();
- if s.is_extending() {
- s.set_is_extending(false);
- } else {
- s.set_select_mode(pending_mode);
- }
- });
- }
- }
-
- fn select_columns(
- &mut self,
- head: DisplayPoint,
- goal_column: u32,
- display_map: &DisplaySnapshot,
- window: &mut Window,
- cx: &mut Context<Self>,
- ) {
- let Some(columnar_state) = self.columnar_selection_state.as_ref() else {
- return;
- };
-
- let tail = match columnar_state {
- ColumnarSelectionState::FromMouse {
- selection_tail,
- display_point,
- } => display_point.unwrap_or_else(|| selection_tail.to_display_point(display_map)),
- ColumnarSelectionState::FromSelection { selection_tail } => {
- selection_tail.to_display_point(display_map)
- }
- };
-
- let start_row = cmp::min(tail.row(), head.row());
- let end_row = cmp::max(tail.row(), head.row());
- let start_column = cmp::min(tail.column(), goal_column);
- let end_column = cmp::max(tail.column(), goal_column);
- let reversed = start_column < tail.column();
-
- let selection_ranges = (start_row.0..=end_row.0)
- .map(DisplayRow)
- .filter_map(|row| {
- if (matches!(columnar_state, ColumnarSelectionState::FromMouse { .. })
- || start_column <= display_map.line_len(row))
- && !display_map.is_block_line(row)
- {
- let start = display_map
- .clip_point(DisplayPoint::new(row, start_column), Bias::Left)
- .to_point(display_map);
- let end = display_map
- .clip_point(DisplayPoint::new(row, end_column), Bias::Right)
- .to_point(display_map);
- if reversed {
- Some(end..start)
- } else {
- Some(start..end)
- }
- } else {
- None
- }
- })
- .collect::<Vec<_>>();
- if selection_ranges.is_empty() {
- return;
- }
-
- let ranges = match columnar_state {
- ColumnarSelectionState::FromMouse { .. } => {
- let mut non_empty_ranges = selection_ranges
- .iter()
- .filter(|selection_range| selection_range.start != selection_range.end)
- .peekable();
- if non_empty_ranges.peek().is_some() {
- non_empty_ranges.cloned().collect()
- } else {
- selection_ranges
- }
- }
- _ => selection_ranges,
- };
-
- self.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
- s.select_ranges(ranges);
- });
- cx.notify();
- }
-
- pub fn has_non_empty_selection(&self, snapshot: &DisplaySnapshot) -> bool {
- self.selections
- .all_adjusted(snapshot)
- .iter()
- .any(|selection| !selection.is_empty())
- }
-
- pub fn has_pending_nonempty_selection(&self) -> bool {
- let pending_nonempty_selection = match self.selections.pending_anchor() {
- Some(Selection { start, end, .. }) => start != end,
- None => false,
- };
-
- pending_nonempty_selection
- || (self.columnar_selection_state.is_some()
- && self.selections.disjoint_anchors().len() > 1)
- }
-
- pub fn has_pending_selection(&self) -> bool {
- self.selections.pending_anchor().is_some() || self.columnar_selection_state.is_some()
- }
-
- pub fn cancel(&mut self, _: &Cancel, window: &mut Window, cx: &mut Context<Self>) {
- self.selection_mark_mode = false;
- self.selection_drag_state = SelectionDragState::None;
-
- if self.dismiss_menus_and_popups(true, window, cx) {
- cx.notify();
- return;
- }
- if self.clear_expanded_diff_hunks(cx) {
- cx.notify();
- return;
- }
- if self.show_git_blame_gutter {
- self.show_git_blame_gutter = false;
- cx.notify();
- return;
- }
-
- if self.mode.is_full()
- && self.change_selections(Default::default(), window, cx, |s| s.try_cancel())
- {
- cx.notify();
- return;
- }
-
- cx.propagate();
- }
-
- pub fn dismiss_menus_and_popups(
- &mut self,
- is_user_requested: bool,
- window: &mut Window,
- cx: &mut Context<Self>,
- ) -> bool {
- let mut dismissed = false;
+ ) -> bool {
+ let mut dismissed = false;
dismissed |= self.take_rename(false, window, cx).is_some();
dismissed |= self.hide_blame_popover(true, cx);
@@ -0,0 +1,1095 @@
+use super::*;
+
+impl GutterDimensions {
+ /// The width of the space reserved for the fold indicators,
+ /// use alongside 'justify_end' and `gutter_width` to
+ /// right align content with the line numbers
+ pub fn fold_area_width(&self) -> Pixels {
+ self.margin + self.right_padding
+ }
+}
+
+impl EditorSnapshot {
+ pub fn render_crease_toggle(
+ &self,
+ buffer_row: MultiBufferRow,
+ row_contains_cursor: bool,
+ editor: Entity<Editor>,
+ window: &mut Window,
+ cx: &mut App,
+ ) -> Option<AnyElement> {
+ let folded = self.is_line_folded(buffer_row);
+ let mut is_foldable = false;
+
+ if let Some(crease) = self
+ .crease_snapshot
+ .query_row(buffer_row, self.buffer_snapshot())
+ {
+ is_foldable = true;
+ match crease {
+ Crease::Inline { render_toggle, .. } | Crease::Block { render_toggle, .. } => {
+ if let Some(render_toggle) = render_toggle {
+ let toggle_callback =
+ Arc::new(move |folded, window: &mut Window, cx: &mut App| {
+ if folded {
+ editor.update(cx, |editor, cx| {
+ editor.fold_at(buffer_row, window, cx)
+ });
+ } else {
+ editor.update(cx, |editor, cx| {
+ editor.unfold_at(buffer_row, window, cx)
+ });
+ }
+ });
+ return Some((render_toggle)(
+ buffer_row,
+ folded,
+ toggle_callback,
+ window,
+ cx,
+ ));
+ }
+ }
+ }
+ }
+
+ is_foldable |= !self.use_lsp_folding_ranges && self.starts_indent(buffer_row);
+
+ if folded || (is_foldable && (row_contains_cursor || self.gutter_hovered)) {
+ Some(
+ Disclosure::new(("gutter_crease", buffer_row.0), !folded)
+ .toggle_state(folded)
+ .on_click(window.listener_for(&editor, move |this, _e, window, cx| {
+ if folded {
+ this.unfold_at(buffer_row, window, cx);
+ } else {
+ this.fold_at(buffer_row, window, cx);
+ }
+ }))
+ .into_any_element(),
+ )
+ } else {
+ None
+ }
+ }
+
+ pub fn render_crease_trailer(
+ &self,
+ buffer_row: MultiBufferRow,
+ window: &mut Window,
+ cx: &mut App,
+ ) -> Option<AnyElement> {
+ let folded = self.is_line_folded(buffer_row);
+ if let Crease::Inline { render_trailer, .. } = self
+ .crease_snapshot
+ .query_row(buffer_row, self.buffer_snapshot())?
+ {
+ let render_trailer = render_trailer.as_ref()?;
+ Some(render_trailer(buffer_row, folded, window, cx))
+ } else {
+ None
+ }
+ }
+}
+
+impl Editor {
+ pub fn toggle_fold(
+ &mut self,
+ _: &actions::ToggleFold,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ if self.buffer_kind(cx) == ItemBufferKind::Singleton {
+ let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
+ let selection = self.selections.newest::<Point>(&display_map);
+
+ let range = if selection.is_empty() {
+ let point = selection.head().to_display_point(&display_map);
+ let start = DisplayPoint::new(point.row(), 0).to_point(&display_map);
+ let end = DisplayPoint::new(point.row(), display_map.line_len(point.row()))
+ .to_point(&display_map);
+ start..end
+ } else {
+ selection.range()
+ };
+ if display_map.folds_in_range(range).next().is_some() {
+ self.unfold_lines(&Default::default(), window, cx)
+ } else {
+ self.fold(&Default::default(), window, cx)
+ }
+ } else {
+ let multi_buffer_snapshot = self.buffer.read(cx).snapshot(cx);
+ let buffer_ids: HashSet<_> = self
+ .selections
+ .disjoint_anchor_ranges()
+ .flat_map(|range| multi_buffer_snapshot.buffer_ids_for_range(range))
+ .collect();
+
+ let should_unfold = buffer_ids
+ .iter()
+ .any(|buffer_id| self.is_buffer_folded(*buffer_id, cx));
+
+ for buffer_id in buffer_ids {
+ if should_unfold {
+ self.unfold_buffer(buffer_id, cx);
+ } else {
+ self.fold_buffer(buffer_id, cx);
+ }
+ }
+ }
+ }
+
+ pub fn toggle_fold_recursive(
+ &mut self,
+ _: &actions::ToggleFoldRecursive,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ let selection = self.selections.newest::<Point>(&self.display_snapshot(cx));
+
+ let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
+ let range = if selection.is_empty() {
+ let point = selection.head().to_display_point(&display_map);
+ let start = DisplayPoint::new(point.row(), 0).to_point(&display_map);
+ let end = DisplayPoint::new(point.row(), display_map.line_len(point.row()))
+ .to_point(&display_map);
+ start..end
+ } else {
+ selection.range()
+ };
+ if display_map.folds_in_range(range).next().is_some() {
+ self.unfold_recursive(&Default::default(), window, cx)
+ } else {
+ self.fold_recursive(&Default::default(), window, cx)
+ }
+ }
+
+ pub fn fold(&mut self, _: &actions::Fold, window: &mut Window, cx: &mut Context<Self>) {
+ if self.buffer_kind(cx) == ItemBufferKind::Singleton {
+ let mut to_fold = Vec::new();
+ let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
+ let selections = self.selections.all_adjusted(&display_map);
+
+ for selection in selections {
+ let range = selection.range().sorted();
+ let buffer_start_row = range.start.row;
+
+ if range.start.row != range.end.row {
+ let mut found = false;
+ let mut row = range.start.row;
+ while row <= range.end.row {
+ if let Some(crease) = display_map.crease_for_buffer_row(MultiBufferRow(row))
+ {
+ found = true;
+ row = crease.range().end.row + 1;
+ to_fold.push(crease);
+ } else {
+ row += 1
+ }
+ }
+ if found {
+ continue;
+ }
+ }
+
+ for row in (0..=range.start.row).rev() {
+ if let Some(crease) = display_map.crease_for_buffer_row(MultiBufferRow(row))
+ && crease.range().end.row >= buffer_start_row
+ {
+ to_fold.push(crease);
+ if row <= range.start.row {
+ break;
+ }
+ }
+ }
+ }
+
+ self.fold_creases(to_fold, true, window, cx);
+ } else {
+ let multi_buffer_snapshot = self.buffer.read(cx).snapshot(cx);
+ let buffer_ids = self
+ .selections
+ .disjoint_anchor_ranges()
+ .flat_map(|range| multi_buffer_snapshot.buffer_ids_for_range(range))
+ .collect::<HashSet<_>>();
+ for buffer_id in buffer_ids {
+ self.fold_buffer(buffer_id, cx);
+ }
+ }
+ }
+
+ pub fn toggle_fold_all(
+ &mut self,
+ _: &actions::ToggleFoldAll,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ let has_folds = if self.buffer.read(cx).is_singleton() {
+ let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
+ let has_folds = display_map
+ .folds_in_range(MultiBufferOffset(0)..display_map.buffer_snapshot().len())
+ .next()
+ .is_some();
+ has_folds
+ } else {
+ let snapshot = self.buffer.read(cx).snapshot(cx);
+ let has_folds = snapshot
+ .all_buffer_ids()
+ .any(|buffer_id| self.is_buffer_folded(buffer_id, cx));
+ has_folds
+ };
+
+ if has_folds {
+ self.unfold_all(&actions::UnfoldAll, window, cx);
+ } else {
+ self.fold_all(&actions::FoldAll, window, cx);
+ }
+ }
+
+ pub fn fold_at_level_1(
+ &mut self,
+ _: &actions::FoldAtLevel1,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ self.fold_at_level(&actions::FoldAtLevel(1), window, cx);
+ }
+
+ pub fn fold_at_level_2(
+ &mut self,
+ _: &actions::FoldAtLevel2,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ self.fold_at_level(&actions::FoldAtLevel(2), window, cx);
+ }
+
+ pub fn fold_at_level_3(
+ &mut self,
+ _: &actions::FoldAtLevel3,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ self.fold_at_level(&actions::FoldAtLevel(3), window, cx);
+ }
+
+ pub fn fold_at_level_4(
+ &mut self,
+ _: &actions::FoldAtLevel4,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ self.fold_at_level(&actions::FoldAtLevel(4), window, cx);
+ }
+
+ pub fn fold_at_level_5(
+ &mut self,
+ _: &actions::FoldAtLevel5,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ self.fold_at_level(&actions::FoldAtLevel(5), window, cx);
+ }
+
+ pub fn fold_at_level_6(
+ &mut self,
+ _: &actions::FoldAtLevel6,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ self.fold_at_level(&actions::FoldAtLevel(6), window, cx);
+ }
+
+ pub fn fold_at_level_7(
+ &mut self,
+ _: &actions::FoldAtLevel7,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ self.fold_at_level(&actions::FoldAtLevel(7), window, cx);
+ }
+
+ pub fn fold_at_level_8(
+ &mut self,
+ _: &actions::FoldAtLevel8,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ self.fold_at_level(&actions::FoldAtLevel(8), window, cx);
+ }
+
+ pub fn fold_at_level_9(
+ &mut self,
+ _: &actions::FoldAtLevel9,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ self.fold_at_level(&actions::FoldAtLevel(9), window, cx);
+ }
+
+ pub fn fold_all(&mut self, _: &actions::FoldAll, window: &mut Window, cx: &mut Context<Self>) {
+ if self.buffer.read(cx).is_singleton() {
+ let mut fold_ranges = Vec::new();
+ let snapshot = self.buffer.read(cx).snapshot(cx);
+
+ for row in 0..snapshot.max_row().0 {
+ if let Some(foldable_range) = self
+ .snapshot(window, cx)
+ .crease_for_buffer_row(MultiBufferRow(row))
+ {
+ fold_ranges.push(foldable_range);
+ }
+ }
+
+ self.fold_creases(fold_ranges, true, window, cx);
+ } else {
+ self.toggle_fold_multiple_buffers = cx.spawn_in(window, async move |editor, cx| {
+ editor
+ .update_in(cx, |editor, _, cx| {
+ let snapshot = editor.buffer.read(cx).snapshot(cx);
+ for buffer_id in snapshot.all_buffer_ids() {
+ editor.fold_buffer(buffer_id, cx);
+ }
+ })
+ .ok();
+ });
+ }
+ }
+
+ pub fn fold_function_bodies(
+ &mut self,
+ _: &actions::FoldFunctionBodies,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ let snapshot = self.buffer.read(cx).snapshot(cx);
+
+ let ranges = snapshot
+ .text_object_ranges(
+ MultiBufferOffset(0)..snapshot.len(),
+ TreeSitterOptions::default(),
+ )
+ .filter_map(|(range, obj)| (obj == TextObject::InsideFunction).then_some(range))
+ .collect::<Vec<_>>();
+
+ let creases = ranges
+ .into_iter()
+ .map(|range| Crease::simple(range, self.display_map.read(cx).fold_placeholder.clone()))
+ .collect();
+
+ self.fold_creases(creases, true, window, cx);
+ }
+
+ pub fn fold_recursive(
+ &mut self,
+ _: &actions::FoldRecursive,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ let mut to_fold = Vec::new();
+ let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
+ let selections = self.selections.all_adjusted(&display_map);
+
+ for selection in selections {
+ let range = selection.range().sorted();
+ let buffer_start_row = range.start.row;
+
+ if range.start.row != range.end.row {
+ let mut found = false;
+ for row in range.start.row..=range.end.row {
+ if let Some(crease) = display_map.crease_for_buffer_row(MultiBufferRow(row)) {
+ found = true;
+ to_fold.push(crease);
+ }
+ }
+ if found {
+ continue;
+ }
+ }
+
+ for row in (0..=range.start.row).rev() {
+ if let Some(crease) = display_map.crease_for_buffer_row(MultiBufferRow(row)) {
+ if crease.range().end.row >= buffer_start_row {
+ to_fold.push(crease);
+ } else {
+ break;
+ }
+ }
+ }
+ }
+
+ self.fold_creases(to_fold, true, window, cx);
+ }
+
+ pub fn fold_at(
+ &mut self,
+ buffer_row: MultiBufferRow,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
+
+ if let Some(crease) = display_map.crease_for_buffer_row(buffer_row) {
+ let autoscroll = self
+ .selections
+ .all::<Point>(&display_map)
+ .iter()
+ .any(|selection| crease.range().overlaps(&selection.range()));
+
+ self.fold_creases(vec![crease], autoscroll, window, cx);
+ }
+ }
+
+ pub fn unfold_lines(&mut self, _: &UnfoldLines, _window: &mut Window, cx: &mut Context<Self>) {
+ if self.buffer_kind(cx) == ItemBufferKind::Singleton {
+ let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
+ let buffer = display_map.buffer_snapshot();
+ let selections = self.selections.all::<Point>(&display_map);
+ let ranges = selections
+ .iter()
+ .map(|s| {
+ let range = s.display_range(&display_map).sorted();
+ let mut start = range.start.to_point(&display_map);
+ let mut end = range.end.to_point(&display_map);
+ start.column = 0;
+ end.column = buffer.line_len(MultiBufferRow(end.row));
+ start..end
+ })
+ .collect::<Vec<_>>();
+
+ self.unfold_ranges(&ranges, true, true, cx);
+ } else {
+ let multi_buffer_snapshot = self.buffer.read(cx).snapshot(cx);
+ let buffer_ids = self
+ .selections
+ .disjoint_anchor_ranges()
+ .flat_map(|range| multi_buffer_snapshot.buffer_ids_for_range(range))
+ .collect::<HashSet<_>>();
+ for buffer_id in buffer_ids {
+ self.unfold_buffer(buffer_id, cx);
+ }
+ }
+ }
+
+ pub fn unfold_recursive(
+ &mut self,
+ _: &UnfoldRecursive,
+ _window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
+ let selections = self.selections.all::<Point>(&display_map);
+ let ranges = selections
+ .iter()
+ .map(|s| {
+ let mut range = s.display_range(&display_map).sorted();
+ *range.start.column_mut() = 0;
+ *range.end.column_mut() = display_map.line_len(range.end.row());
+ let start = range.start.to_point(&display_map);
+ let end = range.end.to_point(&display_map);
+ start..end
+ })
+ .collect::<Vec<_>>();
+
+ self.unfold_ranges(&ranges, true, true, cx);
+ }
+
+ pub fn unfold_at(
+ &mut self,
+ buffer_row: MultiBufferRow,
+ _window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
+
+ let intersection_range = Point::new(buffer_row.0, 0)
+ ..Point::new(
+ buffer_row.0,
+ display_map.buffer_snapshot().line_len(buffer_row),
+ );
+
+ let autoscroll = self
+ .selections
+ .all::<Point>(&display_map)
+ .iter()
+ .any(|selection| RangeExt::overlaps(&selection.range(), &intersection_range));
+
+ self.unfold_ranges(&[intersection_range], true, autoscroll, cx);
+ }
+
+ pub fn unfold_all(
+ &mut self,
+ _: &actions::UnfoldAll,
+ _window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ if self.buffer.read(cx).is_singleton() {
+ let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
+ self.unfold_ranges(
+ &[MultiBufferOffset(0)..display_map.buffer_snapshot().len()],
+ true,
+ true,
+ cx,
+ );
+ } else {
+ self.toggle_fold_multiple_buffers = cx.spawn(async move |editor, cx| {
+ editor
+ .update(cx, |editor, cx| {
+ let snapshot = editor.buffer.read(cx).snapshot(cx);
+ for buffer_id in snapshot.all_buffer_ids() {
+ editor.unfold_buffer(buffer_id, cx);
+ }
+ })
+ .ok();
+ });
+ }
+ }
+
+ pub fn fold_selected_ranges(
+ &mut self,
+ _: &FoldSelectedRanges,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
+ let selections = self.selections.all_adjusted(&display_map);
+ let ranges = selections
+ .into_iter()
+ .map(|s| Crease::simple(s.range(), display_map.fold_placeholder.clone()))
+ .collect::<Vec<_>>();
+ self.fold_creases(ranges, true, window, cx);
+ }
+
+ pub fn fold_ranges<T: ToOffset + Clone>(
+ &mut self,
+ ranges: Vec<Range<T>>,
+ auto_scroll: bool,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
+ let ranges = ranges
+ .into_iter()
+ .map(|r| Crease::simple(r, display_map.fold_placeholder.clone()))
+ .collect::<Vec<_>>();
+ self.fold_creases(ranges, auto_scroll, window, cx);
+ }
+
+ pub fn fold_creases<T: ToOffset + Clone>(
+ &mut self,
+ creases: Vec<Crease<T>>,
+ auto_scroll: bool,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ if creases.is_empty() {
+ return;
+ }
+
+ self.display_map.update(cx, |map, cx| map.fold(creases, cx));
+
+ if auto_scroll {
+ self.request_autoscroll(Autoscroll::fit(), cx);
+ }
+
+ cx.notify();
+
+ self.scrollbar_marker_state.dirty = true;
+ self.update_data_on_scroll(false, window, cx);
+ self.folds_did_change(cx);
+ }
+
+ /// Removes any folds whose ranges intersect any of the given ranges.
+ pub fn unfold_ranges<T: ToOffset + Clone>(
+ &mut self,
+ ranges: &[Range<T>],
+ inclusive: bool,
+ auto_scroll: bool,
+ cx: &mut Context<Self>,
+ ) {
+ self.remove_folds_with(ranges, auto_scroll, cx, |map, cx| {
+ map.unfold_intersecting(ranges.iter().cloned(), inclusive, cx);
+ });
+ self.folds_did_change(cx);
+ }
+
+ pub fn fold_buffer(&mut self, buffer_id: BufferId, cx: &mut Context<Self>) {
+ self.fold_buffers([buffer_id], cx);
+ }
+
+ pub fn fold_buffers(
+ &mut self,
+ buffer_ids: impl IntoIterator<Item = BufferId>,
+ cx: &mut Context<Self>,
+ ) {
+ if self.buffer().read(cx).is_singleton() {
+ return;
+ }
+
+ let ids_to_fold: Vec<BufferId> = buffer_ids
+ .into_iter()
+ .filter(|id| !self.is_buffer_folded(*id, cx))
+ .collect();
+
+ if ids_to_fold.is_empty() {
+ return;
+ }
+
+ self.display_map.update(cx, |display_map, cx| {
+ display_map.fold_buffers(ids_to_fold.clone(), cx)
+ });
+
+ let snapshot = self.display_snapshot(cx);
+ self.selections.change_with(&snapshot, |selections| {
+ for buffer_id in ids_to_fold.iter().copied() {
+ selections.remove_selections_from_buffer(buffer_id);
+ }
+ });
+
+ cx.emit(EditorEvent::BufferFoldToggled {
+ ids: ids_to_fold,
+ folded: true,
+ });
+ cx.notify();
+ }
+
+ pub fn unfold_buffer(&mut self, buffer_id: BufferId, cx: &mut Context<Self>) {
+ if self.buffer().read(cx).is_singleton() || !self.is_buffer_folded(buffer_id, cx) {
+ return;
+ }
+ self.display_map.update(cx, |display_map, cx| {
+ display_map.unfold_buffers([buffer_id], cx);
+ });
+ cx.emit(EditorEvent::BufferFoldToggled {
+ ids: vec![buffer_id],
+ folded: false,
+ });
+ cx.notify();
+ }
+
+ pub fn is_buffer_folded(&self, buffer: BufferId, cx: &App) -> bool {
+ self.display_map.read(cx).is_buffer_folded(buffer)
+ }
+
+ pub fn has_any_buffer_folded(&self, cx: &App) -> bool {
+ if self.buffer().read(cx).is_singleton() {
+ return false;
+ }
+ !self.folded_buffers(cx).is_empty()
+ }
+
+ pub fn folded_buffers<'a>(&self, cx: &'a App) -> &'a HashSet<BufferId> {
+ self.display_map.read(cx).folded_buffers()
+ }
+
+ pub fn disable_header_for_buffer(&mut self, buffer_id: BufferId, cx: &mut Context<Self>) {
+ self.display_map.update(cx, |display_map, cx| {
+ display_map.disable_header_for_buffer(buffer_id, cx);
+ });
+ cx.notify();
+ }
+
+ /// Removes any folds with the given ranges.
+ pub fn remove_folds_with_type<T: ToOffset + Clone>(
+ &mut self,
+ ranges: &[Range<T>],
+ type_id: TypeId,
+ auto_scroll: bool,
+ cx: &mut Context<Self>,
+ ) {
+ self.remove_folds_with(ranges, auto_scroll, cx, |map, cx| {
+ map.remove_folds_with_type(ranges.iter().cloned(), type_id, cx)
+ });
+ self.folds_did_change(cx);
+ }
+
+ pub fn update_renderer_widths(
+ &mut self,
+ widths: impl IntoIterator<Item = (ChunkRendererId, Pixels)>,
+ cx: &mut Context<Self>,
+ ) -> bool {
+ self.display_map
+ .update(cx, |map, cx| map.update_fold_widths(widths, cx))
+ }
+
+ pub fn default_fold_placeholder(&self, cx: &App) -> FoldPlaceholder {
+ self.display_map.read(cx).fold_placeholder.clone()
+ }
+
+ pub fn insert_creases(
+ &mut self,
+ creases: impl IntoIterator<Item = Crease<Anchor>>,
+ cx: &mut Context<Self>,
+ ) -> Vec<CreaseId> {
+ self.display_map
+ .update(cx, |map, cx| map.insert_creases(creases, cx))
+ }
+
+ pub fn remove_creases(
+ &mut self,
+ ids: impl IntoIterator<Item = CreaseId>,
+ cx: &mut Context<Self>,
+ ) -> Vec<(CreaseId, Range<Anchor>)> {
+ self.display_map
+ .update(cx, |map, cx| map.remove_creases(ids, cx))
+ }
+
+ pub(super) fn fold_at_level(
+ &mut self,
+ fold_at: &FoldAtLevel,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ if !self.buffer.read(cx).is_singleton() {
+ return;
+ }
+
+ let fold_at_level = fold_at.0;
+ let snapshot = self.buffer.read(cx).snapshot(cx);
+ let mut to_fold = Vec::new();
+ let mut stack = vec![(0, snapshot.max_row().0, 1)];
+
+ let row_ranges_to_keep: Vec<Range<u32>> = self
+ .selections
+ .all::<Point>(&self.display_snapshot(cx))
+ .into_iter()
+ .map(|sel| sel.start.row..sel.end.row)
+ .collect();
+
+ while let Some((mut start_row, end_row, current_level)) = stack.pop() {
+ while start_row < end_row {
+ match self
+ .snapshot(window, cx)
+ .crease_for_buffer_row(MultiBufferRow(start_row))
+ {
+ Some(crease) => {
+ let nested_start_row = crease.range().start.row + 1;
+ let nested_end_row = crease.range().end.row;
+
+ if current_level < fold_at_level {
+ stack.push((nested_start_row, nested_end_row, current_level + 1));
+ } else if current_level == fold_at_level {
+ // Fold iff there is no selection completely contained within the fold region
+ if !row_ranges_to_keep.iter().any(|selection| {
+ selection.end >= nested_start_row
+ && selection.start <= nested_end_row
+ }) {
+ to_fold.push(crease);
+ }
+ }
+
+ start_row = nested_end_row + 1;
+ }
+ None => start_row += 1,
+ }
+ }
+ }
+
+ self.fold_creases(to_fold, true, window, cx);
+ }
+
+ pub(super) fn unfold_buffers_with_selections(&mut self, cx: &mut Context<Self>) {
+ if self.buffer().read(cx).is_singleton() {
+ return;
+ }
+ let snapshot = self.buffer.read(cx).snapshot(cx);
+ let buffer_ids: HashSet<BufferId> = self
+ .selections
+ .disjoint_anchor_ranges()
+ .flat_map(|range| snapshot.buffer_ids_for_range(range))
+ .collect();
+ for buffer_id in buffer_ids {
+ self.unfold_buffer(buffer_id, cx);
+ }
+ }
+
+ pub(super) fn folds_did_change(&mut self, cx: &mut Context<Self>) {
+ use text::ToOffset as _;
+
+ if self.mode.is_minimap()
+ || WorkspaceSettings::get(None, cx).restore_on_startup
+ == RestoreOnStartupBehavior::EmptyTab
+ {
+ return;
+ }
+
+ let display_snapshot = self
+ .display_map
+ .update(cx, |display_map, cx| display_map.snapshot(cx));
+ let Some(buffer_snapshot) = display_snapshot.buffer_snapshot().as_singleton() else {
+ return;
+ };
+ let inmemory_folds = display_snapshot
+ .folds_in_range(MultiBufferOffset(0)..display_snapshot.buffer_snapshot().len())
+ .map(|fold| {
+ let start = fold.range.start.text_anchor_in(buffer_snapshot);
+ let end = fold.range.end.text_anchor_in(buffer_snapshot);
+ (start..end).to_point(buffer_snapshot)
+ })
+ .collect();
+ self.update_restoration_data(cx, |data| {
+ data.folds = inmemory_folds;
+ });
+
+ let Some(workspace_id) = self.workspace_serialization_id(cx) else {
+ return;
+ };
+
+ // Get file path for path-based fold storage (survives tab close)
+ let Some(file_path) = self.buffer().read(cx).as_singleton().and_then(|buffer| {
+ project::File::from_dyn(buffer.read(cx).file())
+ .map(|file| Arc::<Path>::from(file.abs_path(cx)))
+ }) else {
+ return;
+ };
+
+ let background_executor = cx.background_executor().clone();
+ const FINGERPRINT_LEN: usize = 32;
+ let db_folds = display_snapshot
+ .folds_in_range(MultiBufferOffset(0)..display_snapshot.buffer_snapshot().len())
+ .map(|fold| {
+ let start = fold
+ .range
+ .start
+ .text_anchor_in(buffer_snapshot)
+ .to_offset(buffer_snapshot);
+ let end = fold
+ .range
+ .end
+ .text_anchor_in(buffer_snapshot)
+ .to_offset(buffer_snapshot);
+
+ // Extract fingerprints - content at fold boundaries for validation on restore
+ // Both fingerprints must be INSIDE the fold to avoid capturing surrounding
+ // content that might change independently.
+ // start_fp: first min(32, fold_len) bytes of fold content
+ // end_fp: last min(32, fold_len) bytes of fold content
+ // Clip to character boundaries to handle multibyte UTF-8 characters.
+ let fold_len = end - start;
+ let start_fp_end = buffer_snapshot
+ .clip_offset(start + std::cmp::min(FINGERPRINT_LEN, fold_len), Bias::Left);
+ let start_fp: String = buffer_snapshot
+ .text_for_range(start..start_fp_end)
+ .collect();
+ let end_fp_start = buffer_snapshot
+ .clip_offset(end.saturating_sub(FINGERPRINT_LEN).max(start), Bias::Right);
+ let end_fp: String = buffer_snapshot.text_for_range(end_fp_start..end).collect();
+
+ (start, end, start_fp, end_fp)
+ })
+ .collect::<Vec<_>>();
+ let db = EditorDb::global(cx);
+ self.serialize_folds = cx.background_spawn(async move {
+ background_executor.timer(SERIALIZATION_THROTTLE_TIME).await;
+ if db_folds.is_empty() {
+ // No folds - delete any persisted folds for this file
+ db.delete_file_folds(workspace_id, file_path)
+ .await
+ .with_context(|| format!("deleting file folds for workspace {workspace_id:?}"))
+ .log_err();
+ } else {
+ db.save_file_folds(workspace_id, file_path, db_folds)
+ .await
+ .with_context(|| {
+ format!("persisting file folds for workspace {workspace_id:?}")
+ })
+ .log_err();
+ }
+ });
+ }
+
+ pub(super) fn refresh_single_line_folds(
+ &mut self,
+ window: &mut Window,
+ cx: &mut Context<Editor>,
+ ) {
+ struct NewlineFold;
+ let type_id = std::any::TypeId::of::<NewlineFold>();
+ if !self.mode.is_single_line() {
+ return;
+ }
+ let snapshot = self.snapshot(window, cx);
+ if snapshot.buffer_snapshot().max_point().row == 0 {
+ return;
+ }
+ let task = cx.background_spawn(async move {
+ let new_newlines = snapshot
+ .buffer_chars_at(MultiBufferOffset(0))
+ .filter_map(|(c, i)| {
+ if c == '\n' {
+ Some(
+ snapshot.buffer_snapshot().anchor_after(i)
+ ..snapshot.buffer_snapshot().anchor_before(i + 1usize),
+ )
+ } else {
+ None
+ }
+ })
+ .collect::<Vec<_>>();
+ let existing_newlines = snapshot
+ .folds_in_range(MultiBufferOffset(0)..snapshot.buffer_snapshot().len())
+ .filter_map(|fold| {
+ if fold.placeholder.type_tag == Some(type_id) {
+ Some(fold.range.start..fold.range.end)
+ } else {
+ None
+ }
+ })
+ .collect::<Vec<_>>();
+
+ (new_newlines, existing_newlines)
+ });
+ self.folding_newlines = cx.spawn(async move |this, cx| {
+ let (new_newlines, existing_newlines) = task.await;
+ if new_newlines == existing_newlines {
+ return;
+ }
+ let placeholder = FoldPlaceholder {
+ render: Arc::new(move |_, _, cx| {
+ div()
+ .bg(cx.theme().status().hint_background)
+ .border_b_1()
+ .size_full()
+ .font(ThemeSettings::get_global(cx).buffer_font.clone())
+ .border_color(cx.theme().status().hint)
+ .child("\\n")
+ .into_any()
+ }),
+ constrain_width: false,
+ merge_adjacent: false,
+ type_tag: Some(type_id),
+ collapsed_text: None,
+ };
+ let creases = new_newlines
+ .into_iter()
+ .map(|range| Crease::simple(range, placeholder.clone()))
+ .collect();
+ this.update(cx, |this, cx| {
+ this.display_map.update(cx, |display_map, cx| {
+ display_map.remove_folds_with_type(existing_newlines, type_id, cx);
+ display_map.fold(creases, cx);
+ });
+ })
+ .ok();
+ });
+ }
+
+ /// Load folds from the file_folds database table by file path.
+ /// Used when manually opening a file that was previously closed.
+ pub(super) fn load_folds_from_db(
+ &mut self,
+ workspace_id: WorkspaceId,
+ file_path: PathBuf,
+ window: &mut Window,
+ cx: &mut Context<Editor>,
+ ) {
+ if self.mode.is_minimap()
+ || WorkspaceSettings::get(None, cx).restore_on_startup
+ == RestoreOnStartupBehavior::EmptyTab
+ {
+ return;
+ }
+
+ let Some(folds) = EditorDb::global(cx)
+ .get_file_folds(workspace_id, &file_path)
+ .log_err()
+ else {
+ return;
+ };
+ if folds.is_empty() {
+ return;
+ }
+
+ let snapshot = self.buffer.read(cx).snapshot(cx);
+ let snapshot_len = snapshot.len().0;
+
+ // Helper: search for fingerprint in buffer, return offset if found
+ let find_fingerprint = |fingerprint: &str, search_start: usize| -> Option<usize> {
+ let search_start = snapshot
+ .clip_offset(MultiBufferOffset(search_start), Bias::Left)
+ .0;
+ let search_end = snapshot_len.saturating_sub(fingerprint.len());
+
+ let mut byte_offset = search_start;
+ for ch in snapshot.chars_at(MultiBufferOffset(search_start)) {
+ if byte_offset > search_end {
+ break;
+ }
+ if snapshot.contains_str_at(MultiBufferOffset(byte_offset), fingerprint) {
+ return Some(byte_offset);
+ }
+ byte_offset += ch.len_utf8();
+ }
+ None
+ };
+
+ let mut search_start = 0usize;
+
+ let valid_folds: Vec<_> = folds
+ .into_iter()
+ .filter_map(|(stored_start, stored_end, start_fp, end_fp)| {
+ let sfp = start_fp?;
+ let efp = end_fp?;
+ let efp_len = efp.len();
+
+ let start_matches = stored_start < snapshot_len
+ && snapshot.contains_str_at(MultiBufferOffset(stored_start), &sfp);
+ let efp_check_pos = stored_end.saturating_sub(efp_len);
+ let end_matches = efp_check_pos >= stored_start
+ && stored_end <= snapshot_len
+ && snapshot.contains_str_at(MultiBufferOffset(efp_check_pos), &efp);
+
+ let (new_start, new_end) = if start_matches && end_matches {
+ (stored_start, stored_end)
+ } else if sfp == efp {
+ let new_start = find_fingerprint(&sfp, search_start)?;
+ let fold_len = stored_end - stored_start;
+ let new_end = new_start + fold_len;
+ (new_start, new_end)
+ } else {
+ let new_start = find_fingerprint(&sfp, search_start)?;
+ let efp_pos = find_fingerprint(&efp, new_start + sfp.len())?;
+ let new_end = efp_pos + efp_len;
+ (new_start, new_end)
+ };
+
+ search_start = new_end;
+
+ if new_end <= new_start {
+ return None;
+ }
+
+ Some(
+ snapshot.clip_offset(MultiBufferOffset(new_start), Bias::Left)
+ ..snapshot.clip_offset(MultiBufferOffset(new_end), Bias::Right),
+ )
+ })
+ .collect();
+
+ if !valid_folds.is_empty() {
+ self.fold_ranges(valid_folds, false, window, cx);
+ }
+ }
+
+ fn remove_folds_with<T: ToOffset + Clone>(
+ &mut self,
+ ranges: &[Range<T>],
+ auto_scroll: bool,
+ cx: &mut Context<Self>,
+ update: impl FnOnce(&mut DisplayMap, &mut Context<DisplayMap>),
+ ) {
+ if ranges.is_empty() {
+ return;
+ }
+
+ self.display_map.update(cx, update);
+
+ if auto_scroll {
+ self.request_autoscroll(Autoscroll::fit(), cx);
+ }
+
+ cx.notify();
+ self.scrollbar_marker_state.dirty = true;
+ self.active_indent_guides_state.dirty = true;
+ }
+}
@@ -0,0 +1,899 @@
+use super::*;
+
+impl Editor {
+ pub fn sync_selections(
+ &mut self,
+ other: Entity<Editor>,
+ cx: &mut Context<Self>,
+ ) -> gpui::Subscription {
+ let other_selections = other.read(cx).selections.disjoint_anchors().to_vec();
+ if !other_selections.is_empty() {
+ self.selections
+ .change_with(&self.display_snapshot(cx), |selections| {
+ selections.select_anchors(other_selections);
+ });
+ }
+
+ let other_subscription = cx.subscribe(&other, |this, other, other_evt, cx| {
+ if let EditorEvent::SelectionsChanged { local: true } = other_evt {
+ let other_selections = other.read(cx).selections.disjoint_anchors().to_vec();
+ if other_selections.is_empty() {
+ return;
+ }
+ let snapshot = this.display_snapshot(cx);
+ this.selections.change_with(&snapshot, |selections| {
+ selections.select_anchors(other_selections);
+ });
+ }
+ });
+
+ let this_subscription = cx.subscribe_self::<EditorEvent>(move |this, this_evt, cx| {
+ if let EditorEvent::SelectionsChanged { local: true } = this_evt {
+ let these_selections = this.selections.disjoint_anchors().to_vec();
+ if these_selections.is_empty() {
+ return;
+ }
+ other.update(cx, |other_editor, cx| {
+ let snapshot = other_editor.display_snapshot(cx);
+ other_editor
+ .selections
+ .change_with(&snapshot, |selections| {
+ selections.select_anchors(these_selections);
+ })
+ });
+ }
+ });
+
+ Subscription::join(other_subscription, this_subscription)
+ }
+
+ /// Changes selections using the provided mutation function. Changes to `self.selections` occur
+ /// immediately, but when run within `transact` or `with_selection_effects_deferred` other
+ /// effects of selection change occur at the end of the transaction.
+ pub fn change_selections<R>(
+ &mut self,
+ effects: SelectionEffects,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ change: impl FnOnce(&mut MutableSelectionsCollection<'_, '_>) -> R,
+ ) -> R {
+ let snapshot = self.display_snapshot(cx);
+ if let Some(state) = &mut self.deferred_selection_effects_state {
+ state.effects.scroll = effects.scroll.or(state.effects.scroll);
+ state.effects.completions = effects.completions;
+ state.effects.nav_history = effects.nav_history.or(state.effects.nav_history);
+ let (changed, result) = self.selections.change_with(&snapshot, change);
+ state.changed |= changed;
+ return result;
+ }
+ let mut state = DeferredSelectionEffectsState {
+ changed: false,
+ effects,
+ old_cursor_position: self.selections.newest_anchor().head(),
+ history_entry: SelectionHistoryEntry {
+ selections: self.selections.disjoint_anchors_arc(),
+ select_next_state: self.select_next_state.clone(),
+ select_prev_state: self.select_prev_state.clone(),
+ add_selections_state: self.add_selections_state.clone(),
+ },
+ };
+ let (changed, result) = self.selections.change_with(&snapshot, change);
+ state.changed = state.changed || changed;
+ if self.defer_selection_effects {
+ self.deferred_selection_effects_state = Some(state);
+ } else {
+ self.apply_selection_effects(state, window, cx);
+ }
+ result
+ }
+
+ /// Defers the effects of selection change, so that the effects of multiple calls to
+ /// `change_selections` are applied at the end. This way these intermediate states aren't added
+ /// to selection history and the state of popovers based on selection position aren't
+ /// erroneously updated.
+ pub fn with_selection_effects_deferred<R>(
+ &mut self,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ update: impl FnOnce(&mut Self, &mut Window, &mut Context<Self>) -> R,
+ ) -> R {
+ let already_deferred = self.defer_selection_effects;
+ self.defer_selection_effects = true;
+ let result = update(self, window, cx);
+ if !already_deferred {
+ self.defer_selection_effects = false;
+ if let Some(state) = self.deferred_selection_effects_state.take() {
+ self.apply_selection_effects(state, window, cx);
+ }
+ }
+ result
+ }
+
+ pub fn has_non_empty_selection(&self, snapshot: &DisplaySnapshot) -> bool {
+ self.selections
+ .all_adjusted(snapshot)
+ .iter()
+ .any(|selection| !selection.is_empty())
+ }
+
+ pub fn is_range_selected(&mut self, range: &Range<Anchor>, cx: &mut Context<Self>) -> bool {
+ if self
+ .selections
+ .pending_anchor()
+ .is_some_and(|pending_selection| {
+ let snapshot = self.buffer().read(cx).snapshot(cx);
+ pending_selection.range().includes(range, &snapshot)
+ })
+ {
+ return true;
+ }
+
+ self.selections
+ .disjoint_in_range::<MultiBufferOffset>(range.clone(), &self.display_snapshot(cx))
+ .into_iter()
+ .any(|selection| {
+ // This is needed to cover a corner case, if we just check for an existing
+ // selection in the fold range, having a cursor at the start of the fold
+ // marks it as selected. Non-empty selections don't cause this.
+ let length = selection.end - selection.start;
+ length > 0
+ })
+ }
+
+ pub fn has_pending_nonempty_selection(&self) -> bool {
+ let pending_nonempty_selection = match self.selections.pending_anchor() {
+ Some(Selection { start, end, .. }) => start != end,
+ None => false,
+ };
+
+ pending_nonempty_selection
+ || (self.columnar_selection_state.is_some()
+ && self.selections.disjoint_anchors().len() > 1)
+ }
+
+ pub fn has_pending_selection(&self) -> bool {
+ self.selections.pending_anchor().is_some() || self.columnar_selection_state.is_some()
+ }
+
+ pub fn set_selections_from_remote(
+ &mut self,
+ selections: Vec<Selection<Anchor>>,
+ pending_selection: Option<Selection<Anchor>>,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ let old_cursor_position = self.selections.newest_anchor().head();
+ self.selections
+ .change_with(&self.display_snapshot(cx), |s| {
+ s.select_anchors(selections);
+ if let Some(pending_selection) = pending_selection {
+ s.set_pending(pending_selection, SelectMode::Character);
+ } else {
+ s.clear_pending();
+ }
+ });
+ self.selections_did_change(
+ false,
+ &old_cursor_position,
+ SelectionEffects::default(),
+ window,
+ cx,
+ );
+ }
+
+ pub fn set_mark(&mut self, _: &actions::SetMark, window: &mut Window, cx: &mut Context<Self>) {
+ if self.selection_mark_mode {
+ self.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
+ s.move_with(&mut |_, sel| {
+ sel.collapse_to(sel.head(), SelectionGoal::None);
+ });
+ })
+ }
+ self.selection_mark_mode = true;
+ cx.notify();
+ }
+
+ pub fn swap_selection_ends(
+ &mut self,
+ _: &actions::SwapSelectionEnds,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ self.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
+ s.move_with(&mut |_, sel| {
+ if sel.start != sel.end {
+ sel.reversed = !sel.reversed
+ }
+ });
+ });
+ self.request_autoscroll(Autoscroll::newest(), cx);
+ cx.notify();
+ }
+
+ pub(super) fn select(
+ &mut self,
+ phase: SelectPhase,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ self.hide_context_menu(window, cx);
+
+ match phase {
+ SelectPhase::Begin {
+ position,
+ add,
+ click_count,
+ } => self.begin_selection(position, add, click_count, window, cx),
+ SelectPhase::BeginColumnar {
+ position,
+ goal_column,
+ reset,
+ mode,
+ } => self.begin_columnar_selection(position, goal_column, reset, mode, window, cx),
+ SelectPhase::Extend {
+ position,
+ click_count,
+ } => self.extend_selection(position, click_count, window, cx),
+ SelectPhase::Update {
+ position,
+ goal_column,
+ scroll_delta,
+ } => self.update_selection(position, goal_column, scroll_delta, window, cx),
+ SelectPhase::End => self.end_selection(window, cx),
+ }
+ }
+
+ pub(super) fn extend_selection(
+ &mut self,
+ position: DisplayPoint,
+ click_count: usize,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
+ let tail = self
+ .selections
+ .newest::<MultiBufferOffset>(&display_map)
+ .tail();
+ let click_count = click_count.max(match self.selections.select_mode() {
+ SelectMode::Character => 1,
+ SelectMode::Word(_) => 2,
+ SelectMode::Line(_) => 3,
+ SelectMode::All => 4,
+ });
+ self.begin_selection(position, false, click_count, window, cx);
+
+ let tail_anchor = display_map.buffer_snapshot().anchor_before(tail);
+
+ let current_selection = match self.selections.select_mode() {
+ SelectMode::Character | SelectMode::All => tail_anchor..tail_anchor,
+ SelectMode::Word(range) | SelectMode::Line(range) => range.clone(),
+ };
+
+ let Some((mut pending_selection, mut pending_mode)) = self.pending_selection_and_mode()
+ else {
+ log::error!("extend_selection dispatched with no pending selection");
+ return;
+ };
+
+ if pending_selection
+ .start
+ .cmp(¤t_selection.start, display_map.buffer_snapshot())
+ == Ordering::Greater
+ {
+ pending_selection.start = current_selection.start;
+ }
+ if pending_selection
+ .end
+ .cmp(¤t_selection.end, display_map.buffer_snapshot())
+ == Ordering::Less
+ {
+ pending_selection.end = current_selection.end;
+ pending_selection.reversed = true;
+ }
+
+ match &mut pending_mode {
+ SelectMode::Word(range) | SelectMode::Line(range) => *range = current_selection,
+ _ => {}
+ }
+
+ let effects = if EditorSettings::get_global(cx).autoscroll_on_clicks {
+ SelectionEffects::scroll(Autoscroll::fit())
+ } else {
+ SelectionEffects::no_scroll()
+ };
+
+ self.change_selections(effects, window, cx, |s| {
+ s.set_pending(pending_selection.clone(), pending_mode);
+ s.set_is_extending(true);
+ });
+ }
+
+ pub(super) fn begin_selection(
+ &mut self,
+ position: DisplayPoint,
+ add: bool,
+ click_count: usize,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ if !self.focus_handle.is_focused(window) {
+ self.last_focused_descendant = None;
+ window.focus(&self.focus_handle, cx);
+ }
+
+ let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
+ let buffer = display_map.buffer_snapshot();
+ let position = display_map.clip_point(position, Bias::Left);
+
+ let start;
+ let end;
+ let mode;
+ let mut auto_scroll;
+ match click_count {
+ 1 => {
+ start = buffer.anchor_before(position.to_point(&display_map));
+ end = start;
+ mode = SelectMode::Character;
+ auto_scroll = true;
+ }
+ 2 => {
+ let position = display_map
+ .clip_point(position, Bias::Left)
+ .to_offset(&display_map, Bias::Left);
+ let (range, _) = buffer.surrounding_word(position, None);
+ start = buffer.anchor_before(range.start);
+ end = buffer.anchor_before(range.end);
+ mode = SelectMode::Word(start..end);
+ auto_scroll = true;
+ }
+ 3 => {
+ let position = display_map
+ .clip_point(position, Bias::Left)
+ .to_point(&display_map);
+ let line_start = display_map.prev_line_boundary(position).0;
+ let next_line_start = buffer.clip_point(
+ display_map.next_line_boundary(position).0 + Point::new(1, 0),
+ Bias::Left,
+ );
+ start = buffer.anchor_before(line_start);
+ end = buffer.anchor_before(next_line_start);
+ mode = SelectMode::Line(start..end);
+ auto_scroll = true;
+ }
+ _ => {
+ start = buffer.anchor_before(MultiBufferOffset(0));
+ end = buffer.anchor_before(buffer.len());
+ mode = SelectMode::All;
+ auto_scroll = false;
+ }
+ }
+ auto_scroll &= EditorSettings::get_global(cx).autoscroll_on_clicks;
+
+ let point_to_delete: Option<usize> = {
+ let selected_points: Vec<Selection<Point>> =
+ self.selections.disjoint_in_range(start..end, &display_map);
+
+ if !add || click_count > 1 {
+ None
+ } else if !selected_points.is_empty() {
+ Some(selected_points[0].id)
+ } else {
+ let clicked_point_already_selected =
+ self.selections.disjoint_anchors().iter().find(|selection| {
+ selection.start.to_point(buffer) == start.to_point(buffer)
+ || selection.end.to_point(buffer) == end.to_point(buffer)
+ });
+
+ clicked_point_already_selected.map(|selection| selection.id)
+ }
+ };
+
+ let selections_count = self.selections.count();
+ let effects = if auto_scroll {
+ SelectionEffects::default()
+ } else {
+ SelectionEffects::no_scroll()
+ };
+
+ self.change_selections(effects, window, cx, |s| {
+ if let Some(point_to_delete) = point_to_delete {
+ s.delete(point_to_delete);
+
+ if selections_count == 1 {
+ s.set_pending_anchor_range(start..end, mode);
+ }
+ } else {
+ if !add {
+ s.clear_disjoint();
+ }
+
+ s.set_pending_anchor_range(start..end, mode);
+ }
+ });
+ }
+
+ pub(super) fn begin_columnar_selection(
+ &mut self,
+ position: DisplayPoint,
+ goal_column: u32,
+ reset: bool,
+ mode: ColumnarMode,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ if !self.focus_handle.is_focused(window) {
+ self.last_focused_descendant = None;
+ window.focus(&self.focus_handle, cx);
+ }
+
+ let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
+
+ if reset {
+ let pointer_position = display_map
+ .buffer_snapshot()
+ .anchor_before(position.to_point(&display_map));
+
+ self.change_selections(
+ SelectionEffects::scroll(Autoscroll::newest()),
+ window,
+ cx,
+ |s| {
+ s.clear_disjoint();
+ s.set_pending_anchor_range(
+ pointer_position..pointer_position,
+ SelectMode::Character,
+ );
+ },
+ );
+ };
+
+ let tail = self.selections.newest::<Point>(&display_map).tail();
+ let selection_anchor = display_map.buffer_snapshot().anchor_before(tail);
+ self.columnar_selection_state = match mode {
+ ColumnarMode::FromMouse => Some(ColumnarSelectionState::FromMouse {
+ selection_tail: selection_anchor,
+ display_point: if reset {
+ if position.column() != goal_column {
+ Some(DisplayPoint::new(position.row(), goal_column))
+ } else {
+ None
+ }
+ } else {
+ None
+ },
+ }),
+ ColumnarMode::FromSelection => Some(ColumnarSelectionState::FromSelection {
+ selection_tail: selection_anchor,
+ }),
+ };
+
+ if !reset {
+ self.select_columns(position, goal_column, &display_map, window, cx);
+ }
+ }
+
+ pub(super) fn update_selection(
+ &mut self,
+ position: DisplayPoint,
+ goal_column: u32,
+ scroll_delta: gpui::Point<f32>,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
+
+ if self.columnar_selection_state.is_some() {
+ self.select_columns(position, goal_column, &display_map, window, cx);
+ } else if let Some((mut pending, mode)) = self.pending_selection_and_mode() {
+ let buffer = display_map.buffer_snapshot();
+ let head;
+ let tail;
+ match &mode {
+ SelectMode::Character => {
+ head = position.to_point(&display_map);
+ tail = pending.tail().to_point(buffer);
+ }
+ SelectMode::Word(original_range) => {
+ let offset = display_map
+ .clip_point(position, Bias::Left)
+ .to_offset(&display_map, Bias::Left);
+ let original_range = original_range.to_offset(buffer);
+
+ let head_offset = if buffer.is_inside_word(offset, None)
+ || original_range.contains(&offset)
+ {
+ let (word_range, _) = buffer.surrounding_word(offset, None);
+ if word_range.start < original_range.start {
+ word_range.start
+ } else {
+ word_range.end
+ }
+ } else {
+ offset
+ };
+
+ head = head_offset.to_point(buffer);
+ if head_offset <= original_range.start {
+ tail = original_range.end.to_point(buffer);
+ } else {
+ tail = original_range.start.to_point(buffer);
+ }
+ }
+ SelectMode::Line(original_range) => {
+ let original_range = original_range.to_point(display_map.buffer_snapshot());
+
+ let position = display_map
+ .clip_point(position, Bias::Left)
+ .to_point(&display_map);
+ let line_start = display_map.prev_line_boundary(position).0;
+ let next_line_start = buffer.clip_point(
+ display_map.next_line_boundary(position).0 + Point::new(1, 0),
+ Bias::Left,
+ );
+
+ if line_start < original_range.start {
+ head = line_start
+ } else {
+ head = next_line_start
+ }
+
+ if head <= original_range.start {
+ tail = original_range.end;
+ } else {
+ tail = original_range.start;
+ }
+ }
+ SelectMode::All => {
+ return;
+ }
+ };
+
+ if head < tail {
+ pending.start = buffer.anchor_before(head);
+ pending.end = buffer.anchor_before(tail);
+ pending.reversed = true;
+ } else {
+ pending.start = buffer.anchor_before(tail);
+ pending.end = buffer.anchor_before(head);
+ pending.reversed = false;
+ }
+
+ self.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
+ s.set_pending(pending.clone(), mode);
+ });
+ } else {
+ log::error!("update_selection dispatched with no pending selection");
+ return;
+ }
+
+ self.apply_scroll_delta(scroll_delta, window, cx);
+ cx.notify();
+ }
+
+ pub(super) fn end_selection(&mut self, window: &mut Window, cx: &mut Context<Self>) {
+ self.columnar_selection_state.take();
+ if let Some(pending_mode) = self.selections.pending_mode() {
+ let selections = self
+ .selections
+ .all::<MultiBufferOffset>(&self.display_snapshot(cx));
+ self.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
+ s.select(selections);
+ s.clear_pending();
+ if s.is_extending() {
+ s.set_is_extending(false);
+ } else {
+ s.set_select_mode(pending_mode);
+ }
+ });
+ }
+ }
+
+ fn selections_did_change(
+ &mut self,
+ local: bool,
+ old_cursor_position: &Anchor,
+ effects: SelectionEffects,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ self.last_selection_from_search = effects.from_search;
+ window.invalidate_character_coordinates();
+
+ // Copy selections to primary selection buffer
+ #[cfg(any(target_os = "linux", target_os = "freebsd"))]
+ if local {
+ let selections = self
+ .selections
+ .all::<MultiBufferOffset>(&self.display_snapshot(cx));
+ let buffer_handle = self.buffer.read(cx).read(cx);
+
+ let mut text = String::new();
+ for (index, selection) in selections.iter().enumerate() {
+ let text_for_selection = buffer_handle
+ .text_for_range(selection.start..selection.end)
+ .collect::<String>();
+
+ text.push_str(&text_for_selection);
+ if index != selections.len() - 1 {
+ text.push('\n');
+ }
+ }
+
+ if !text.is_empty() {
+ cx.write_to_primary(ClipboardItem::new_string(text));
+ }
+ }
+
+ let selection_anchors = self.selections.disjoint_anchors_arc();
+
+ if self.focus_handle.is_focused(window) && self.leader_id.is_none() {
+ self.buffer.update(cx, |buffer, cx| {
+ buffer.set_active_selections(
+ &selection_anchors,
+ self.selections.line_mode(),
+ self.cursor_shape,
+ cx,
+ )
+ });
+ }
+ let display_map = self
+ .display_map
+ .update(cx, |display_map, cx| display_map.snapshot(cx));
+ let buffer = display_map.buffer_snapshot();
+ if self.selections.count() == 1 {
+ self.add_selections_state = None;
+ }
+ self.select_next_state = None;
+ self.select_prev_state = None;
+ self.select_syntax_node_history.try_clear();
+ self.invalidate_autoclose_regions(&selection_anchors, buffer);
+ self.snippet_stack.invalidate(&selection_anchors, buffer);
+ self.take_rename(false, window, cx);
+
+ let newest_selection = self.selections.newest_anchor();
+ let new_cursor_position = newest_selection.head();
+ let selection_start = newest_selection.start;
+
+ if effects.nav_history.is_none() || effects.nav_history == Some(true) {
+ self.push_to_nav_history(
+ *old_cursor_position,
+ Some(new_cursor_position.to_point(buffer)),
+ false,
+ effects.nav_history == Some(true),
+ cx,
+ );
+ }
+
+ if local {
+ if let Some((anchor, _)) = buffer.anchor_to_buffer_anchor(new_cursor_position) {
+ self.register_buffer(anchor.buffer_id, cx);
+ }
+
+ let mut context_menu = self.context_menu.borrow_mut();
+ let completion_menu = match context_menu.as_ref() {
+ Some(CodeContextMenu::Completions(menu)) => Some(menu),
+ Some(CodeContextMenu::CodeActions(_)) => {
+ *context_menu = None;
+ None
+ }
+ None => None,
+ };
+ let completion_position = completion_menu.map(|menu| menu.initial_position);
+ drop(context_menu);
+
+ if effects.completions
+ && let Some(completion_position) = completion_position
+ {
+ let start_offset = selection_start.to_offset(buffer);
+ let position_matches = start_offset == completion_position.to_offset(buffer);
+ let continue_showing = if let Some((snap, ..)) =
+ buffer.point_to_buffer_offset(completion_position)
+ && !snap.capability.editable()
+ {
+ false
+ } else if position_matches {
+ if self.snippet_stack.is_empty() {
+ buffer.char_kind_before(start_offset, Some(CharScopeContext::Completion))
+ == Some(CharKind::Word)
+ } else {
+ // Snippet choices can be shown even when the cursor is in whitespace.
+ // Dismissing the menu with actions like backspace is handled by
+ // invalidation regions.
+ true
+ }
+ } else {
+ false
+ };
+
+ if continue_showing {
+ self.open_or_update_completions_menu(None, None, false, window, cx);
+ } else {
+ self.hide_context_menu(window, cx);
+ }
+ }
+
+ hide_hover(self, cx);
+
+ self.refresh_code_actions_for_selection(window, cx);
+ self.refresh_document_highlights(cx);
+ refresh_linked_ranges(self, window, cx);
+
+ self.refresh_selected_text_highlights(&display_map, false, window, cx);
+ self.refresh_matching_bracket_highlights(&display_map, cx);
+ self.refresh_outline_symbols_at_cursor(cx);
+ self.update_visible_edit_prediction(window, cx);
+ self.hide_blame_popover(true, cx);
+ if self.git_blame_inline_enabled {
+ self.start_inline_blame_timer(window, cx);
+ }
+ }
+
+ self.blink_manager.update(cx, BlinkManager::pause_blinking);
+
+ if local && !self.suppress_selection_callback {
+ if let Some(callback) = self.on_local_selections_changed.as_ref() {
+ let cursor_position = self.selections.newest::<Point>(&display_map).head();
+ callback(cursor_position, window, cx);
+ }
+ }
+
+ cx.emit(EditorEvent::SelectionsChanged { local });
+
+ let selections = &self.selections.disjoint_anchors_arc();
+ if local && let Some(buffer_snapshot) = buffer.as_singleton() {
+ let inmemory_selections = selections
+ .iter()
+ .map(|s| {
+ let start = s.range().start.text_anchor_in(buffer_snapshot);
+ let end = s.range().end.text_anchor_in(buffer_snapshot);
+ (start..end).to_point(buffer_snapshot)
+ })
+ .collect();
+ self.update_restoration_data(cx, |data| {
+ data.selections = inmemory_selections;
+ });
+
+ if WorkspaceSettings::get(None, cx).restore_on_startup
+ != RestoreOnStartupBehavior::EmptyTab
+ && let Some(workspace_id) = self.workspace_serialization_id(cx)
+ {
+ let snapshot = self.buffer().read(cx).snapshot(cx);
+ let selections = selections.clone();
+ let background_executor = cx.background_executor().clone();
+ let editor_id = cx.entity().entity_id().as_u64() as ItemId;
+ let db = EditorDb::global(cx);
+ self.serialize_selections = cx.background_spawn(async move {
+ background_executor.timer(SERIALIZATION_THROTTLE_TIME).await;
+ let db_selections = selections
+ .iter()
+ .map(|selection| {
+ (
+ selection.start.to_offset(&snapshot).0,
+ selection.end.to_offset(&snapshot).0,
+ )
+ })
+ .collect();
+
+ db.save_editor_selections(editor_id, workspace_id, db_selections)
+ .await
+ .with_context(|| {
+ format!(
+ "persisting editor selections for editor {editor_id}, \
+ workspace {workspace_id:?}"
+ )
+ })
+ .log_err();
+ });
+ }
+ }
+
+ cx.notify();
+ }
+
+ fn apply_selection_effects(
+ &mut self,
+ state: DeferredSelectionEffectsState,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ if state.changed {
+ self.selection_history.push(state.history_entry);
+
+ if let Some(autoscroll) = state.effects.scroll {
+ self.request_autoscroll(autoscroll, cx);
+ }
+
+ let old_cursor_position = &state.old_cursor_position;
+
+ self.selections_did_change(true, old_cursor_position, state.effects, window, cx);
+
+ if self.should_open_signature_help_automatically(old_cursor_position, cx) {
+ self.show_signature_help_auto(window, cx);
+ }
+ }
+ }
+
+ fn select_columns(
+ &mut self,
+ head: DisplayPoint,
+ goal_column: u32,
+ display_map: &DisplaySnapshot,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ let Some(columnar_state) = self.columnar_selection_state.as_ref() else {
+ return;
+ };
+
+ let tail = match columnar_state {
+ ColumnarSelectionState::FromMouse {
+ selection_tail,
+ display_point,
+ } => display_point.unwrap_or_else(|| selection_tail.to_display_point(display_map)),
+ ColumnarSelectionState::FromSelection { selection_tail } => {
+ selection_tail.to_display_point(display_map)
+ }
+ };
+
+ let start_row = cmp::min(tail.row(), head.row());
+ let end_row = cmp::max(tail.row(), head.row());
+ let start_column = cmp::min(tail.column(), goal_column);
+ let end_column = cmp::max(tail.column(), goal_column);
+ let reversed = start_column < tail.column();
+
+ let selection_ranges = (start_row.0..=end_row.0)
+ .map(DisplayRow)
+ .filter_map(|row| {
+ if (matches!(columnar_state, ColumnarSelectionState::FromMouse { .. })
+ || start_column <= display_map.line_len(row))
+ && !display_map.is_block_line(row)
+ {
+ let start = display_map
+ .clip_point(DisplayPoint::new(row, start_column), Bias::Left)
+ .to_point(display_map);
+ let end = display_map
+ .clip_point(DisplayPoint::new(row, end_column), Bias::Right)
+ .to_point(display_map);
+ if reversed {
+ Some(end..start)
+ } else {
+ Some(start..end)
+ }
+ } else {
+ None
+ }
+ })
+ .collect::<Vec<_>>();
+ if selection_ranges.is_empty() {
+ return;
+ }
+
+ let ranges = match columnar_state {
+ ColumnarSelectionState::FromMouse { .. } => {
+ let mut non_empty_ranges = selection_ranges
+ .iter()
+ .filter(|selection_range| selection_range.start != selection_range.end)
+ .peekable();
+ if non_empty_ranges.peek().is_some() {
+ non_empty_ranges.cloned().collect()
+ } else {
+ selection_ranges
+ }
+ }
+ _ => selection_ranges,
+ };
+
+ self.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
+ s.select_ranges(ranges);
+ });
+ cx.notify();
+ }
+
+ fn pending_selection_and_mode(&self) -> Option<(Selection<Anchor>, SelectMode)> {
+ Some((
+ self.selections.pending_anchor()?.clone(),
+ self.selections.pending_mode()?,
+ ))
+ }
+}