diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 175b430ff014cfbb2725c1404c5e22589856c682..608895da9c984a5ee76d804f1ff1b135dd626e8d 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -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, cx: &mut Context) -> 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::(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.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::(&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::(); - - 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::(&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) { - 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::::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::>(); - 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, - cx: &mut Context, - ) -> 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::(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) { - if self.buffer().read(cx).is_singleton() { - return; - } - let snapshot = self.buffer.read(cx).snapshot(cx); - let buffer_ids: HashSet = 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( - &mut self, - effects: SelectionEffects, - window: &mut Window, - cx: &mut Context, - 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( - &mut self, - window: &mut Window, - cx: &mut Context, - update: impl FnOnce(&mut Self, &mut Window, &mut Context) -> 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, - ) { - 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(&mut self, edits: I, cx: &mut Context) where I: IntoIterator, T)>, @@ -4118,515 +3646,41 @@ impl Editor { }); } - fn select(&mut self, phase: SelectPhase, window: &mut Window, cx: &mut Context) { - self.hide_context_menu(window, cx); + pub fn cancel(&mut self, _: &Cancel, window: &mut Window, cx: &mut Context) { + 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, - ) { - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let tail = self - .selections - .newest::(&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, - ) { - 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 = { - let selected_points: Vec> = - 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, - ) { - 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::(&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, - window: &mut Window, - cx: &mut Context, - ) { - 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.columnar_selection_state.take(); - if let Some(pending_mode) = self.selections.pending_mode() { - let selections = self - .selections - .all::(&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, - ) { - 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::>(); - 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.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, - ) -> 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); @@ -6398,78 +5452,6 @@ impl Editor { }) } - fn refresh_single_line_folds(&mut self, window: &mut Window, cx: &mut Context) { - struct NewlineFold; - let type_id = std::any::TypeId::of::(); - 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::>(); - 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::>(); - - (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(); - }); - } - #[ztracing::instrument(skip_all)] fn refresh_outline_symbols_at_cursor(&mut self, cx: &mut Context) { if !self.lsp_data_enabled() { @@ -18190,921 +17172,167 @@ impl Editor { buffer.push_transaction(&transaction.0, cx); } cx.notify(); - }); - Ok(()) - }) - } - - pub fn restart_language_server( - &mut self, - _: &RestartLanguageServer, - _: &mut Window, - cx: &mut Context, - ) { - if let Some(project) = self.project.clone() { - self.buffer.update(cx, |multi_buffer, cx| { - project.update(cx, |project, cx| { - project.restart_language_servers_for_buffers( - multi_buffer.all_buffers().into_iter().collect(), - HashSet::default(), - cx, - ); - }); - }) - } - } - - pub fn stop_language_server( - &mut self, - _: &StopLanguageServer, - _: &mut Window, - cx: &mut Context, - ) { - if let Some(project) = self.project.clone() { - self.buffer.update(cx, |multi_buffer, cx| { - project.update(cx, |project, cx| { - project.stop_language_servers_for_buffers( - multi_buffer.all_buffers().into_iter().collect(), - HashSet::default(), - cx, - ); - }); - }); - } - } - - fn cancel_language_server_work( - workspace: &mut Workspace, - _: &actions::CancelLanguageServerWork, - _: &mut Window, - cx: &mut Context, - ) { - let project = workspace.project(); - let buffers = workspace - .active_item(cx) - .and_then(|item| item.act_as::(cx)) - .map_or(HashSet::default(), |editor| { - editor.read(cx).buffer.read(cx).all_buffers() - }); - project.update(cx, |project, cx| { - project.cancel_language_server_work_for_buffers(buffers, cx); - }); - } - - fn show_character_palette( - &mut self, - _: &ShowCharacterPalette, - window: &mut Window, - _: &mut Context, - ) { - window.show_character_palette(); - } - - pub fn toggle_minimap( - &mut self, - _: &ToggleMinimap, - window: &mut Window, - cx: &mut Context, - ) { - if self.supports_minimap(cx) { - self.set_minimap_visibility(self.minimap_visibility.toggle_visibility(), window, cx); - } - } - - pub fn set_selections_from_remote( - &mut self, - selections: Vec>, - pending_selection: Option>, - window: &mut Window, - cx: &mut Context, - ) { - 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 transact( - &mut self, - window: &mut Window, - cx: &mut Context, - update: impl FnOnce(&mut Self, &mut Window, &mut Context), - ) -> Option { - self.with_selection_effects_deferred(window, cx, |this, window, cx| { - this.start_transaction_at(Instant::now(), window, cx); - update(this, window, cx); - this.end_transaction_at(Instant::now(), cx) - }) - } - - pub fn start_transaction_at( - &mut self, - now: Instant, - window: &mut Window, - cx: &mut Context, - ) -> Option { - self.end_selection(window, cx); - if let Some(tx_id) = self - .buffer - .update(cx, |buffer, cx| buffer.start_transaction_at(now, cx)) - { - self.selection_history - .insert_transaction(tx_id, self.selections.disjoint_anchors_arc()); - cx.emit(EditorEvent::TransactionBegun { - transaction_id: tx_id, - }); - Some(tx_id) - } else { - None - } - } - - pub fn end_transaction_at( - &mut self, - now: Instant, - cx: &mut Context, - ) -> Option { - if let Some(transaction_id) = self - .buffer - .update(cx, |buffer, cx| buffer.end_transaction_at(now, cx)) - { - if let Some((_, end_selections)) = - self.selection_history.transaction_mut(transaction_id) - { - *end_selections = Some(self.selections.disjoint_anchors_arc()); - } else { - log::error!("unexpectedly ended a transaction that wasn't started by this editor"); - } - - cx.emit(EditorEvent::Edited { transaction_id }); - Some(transaction_id) - } else { - None - } - } - - pub fn modify_transaction_selection_history( - &mut self, - transaction_id: TransactionId, - modify: impl FnOnce(&mut (Arc<[Selection]>, Option]>>)), - ) -> bool { - self.selection_history - .transaction_mut(transaction_id) - .map(modify) - .is_some() - } - - pub fn set_mark(&mut self, _: &actions::SetMark, window: &mut Window, cx: &mut Context) { - 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.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 fn toggle_focus( - workspace: &mut Workspace, - _: &actions::ToggleFocus, - window: &mut Window, - cx: &mut Context, - ) { - let Some(item) = workspace.recent_active_item_by_type::(cx) else { - return; - }; - workspace.activate_item(&item, true, true, window, cx); - } - - pub fn toggle_fold( - &mut self, - _: &actions::ToggleFold, - window: &mut Window, - cx: &mut Context, - ) { - 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::(&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, - ) { - let selection = self.selections.newest::(&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) { - 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::>(); - 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, - ) { - 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); - } - } - - fn fold_at_level( - &mut self, - fold_at: &FoldAtLevel, - window: &mut Window, - cx: &mut Context, - ) { - 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> = self - .selections - .all::(&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 fn fold_at_level_1( - &mut self, - _: &actions::FoldAtLevel1, - window: &mut Window, - cx: &mut Context, - ) { - 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.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.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.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.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.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.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.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.fold_at_level(&actions::FoldAtLevel(9), window, cx); - } - - pub fn fold_all(&mut self, _: &actions::FoldAll, window: &mut Window, cx: &mut Context) { - 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, - ) { - 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::>(); - - 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, - ) { - 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, - ) { - 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::(&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) { - 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::(&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::>(); - - 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::>(); - 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, - ) { - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let selections = self.selections.all::(&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::>(); - - self.unfold_ranges(&ranges, true, true, cx); - } - - pub fn unfold_at( - &mut self, - buffer_row: MultiBufferRow, - _window: &mut Window, - cx: &mut Context, - ) { - 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::(&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, - ) { - 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, - ) { - 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::>(); - self.fold_creases(ranges, true, window, cx); - } - - pub fn fold_ranges( - &mut self, - ranges: Vec>, - auto_scroll: bool, - window: &mut Window, - cx: &mut Context, - ) { - 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::>(); - self.fold_creases(ranges, auto_scroll, window, cx); - } - - pub fn fold_creases( - &mut self, - creases: Vec>, - auto_scroll: bool, - window: &mut Window, - cx: &mut Context, - ) { - 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); + }); + Ok(()) + }) } - /// Removes any folds whose ranges intersect any of the given ranges. - pub fn unfold_ranges( + pub fn restart_language_server( &mut self, - ranges: &[Range], - inclusive: bool, - auto_scroll: bool, + _: &RestartLanguageServer, + _: &mut Window, cx: &mut Context, ) { - 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.fold_buffers([buffer_id], cx); + if let Some(project) = self.project.clone() { + self.buffer.update(cx, |multi_buffer, cx| { + project.update(cx, |project, cx| { + project.restart_language_servers_for_buffers( + multi_buffer.all_buffers().into_iter().collect(), + HashSet::default(), + cx, + ); + }); + }) + } } - pub fn fold_buffers( + pub fn stop_language_server( &mut self, - buffer_ids: impl IntoIterator, + _: &StopLanguageServer, + _: &mut Window, cx: &mut Context, ) { - if self.buffer().read(cx).is_singleton() { - return; - } - - let ids_to_fold: Vec = buffer_ids - .into_iter() - .filter(|id| !self.is_buffer_folded(*id, cx)) - .collect(); - - if ids_to_fold.is_empty() { - return; + if let Some(project) = self.project.clone() { + self.buffer.update(cx, |multi_buffer, cx| { + project.update(cx, |project, cx| { + project.stop_language_servers_for_buffers( + multi_buffer.all_buffers().into_iter().collect(), + HashSet::default(), + cx, + ); + }); + }); } - - 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) { - 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, + fn cancel_language_server_work( + workspace: &mut Workspace, + _: &actions::CancelLanguageServerWork, + _: &mut Window, + cx: &mut Context, + ) { + let project = workspace.project(); + let buffers = workspace + .active_item(cx) + .and_then(|item| item.act_as::(cx)) + .map_or(HashSet::default(), |editor| { + editor.read(cx).buffer.read(cx).all_buffers() + }); + project.update(cx, |project, cx| { + project.cancel_language_server_work_for_buffers(buffers, cx); }); - cx.notify(); } - pub fn is_buffer_folded(&self, buffer: BufferId, cx: &App) -> bool { - self.display_map.read(cx).is_buffer_folded(buffer) + fn show_character_palette( + &mut self, + _: &ShowCharacterPalette, + window: &mut Window, + _: &mut Context, + ) { + window.show_character_palette(); } - pub fn has_any_buffer_folded(&self, cx: &App) -> bool { - if self.buffer().read(cx).is_singleton() { - return false; + pub fn toggle_minimap( + &mut self, + _: &ToggleMinimap, + window: &mut Window, + cx: &mut Context, + ) { + if self.supports_minimap(cx) { + self.set_minimap_visibility(self.minimap_visibility.toggle_visibility(), window, cx); } - !self.folded_buffers(cx).is_empty() - } - - pub fn folded_buffers<'a>(&self, cx: &'a App) -> &'a HashSet { - self.display_map.read(cx).folded_buffers() - } - - pub fn disable_header_for_buffer(&mut self, buffer_id: BufferId, cx: &mut Context) { - 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( + pub fn transact( &mut self, - ranges: &[Range], - type_id: TypeId, - auto_scroll: bool, + window: &mut Window, cx: &mut Context, - ) { - 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); + update: impl FnOnce(&mut Self, &mut Window, &mut Context), + ) -> Option { + self.with_selection_effects_deferred(window, cx, |this, window, cx| { + this.start_transaction_at(Instant::now(), window, cx); + update(this, window, cx); + this.end_transaction_at(Instant::now(), cx) + }) } - fn remove_folds_with( + pub fn start_transaction_at( &mut self, - ranges: &[Range], - auto_scroll: bool, + now: Instant, + window: &mut Window, cx: &mut Context, - update: impl FnOnce(&mut DisplayMap, &mut Context), - ) { - if ranges.is_empty() { - return; + ) -> Option { + self.end_selection(window, cx); + if let Some(tx_id) = self + .buffer + .update(cx, |buffer, cx| buffer.start_transaction_at(now, cx)) + { + self.selection_history + .insert_transaction(tx_id, self.selections.disjoint_anchors_arc()); + cx.emit(EditorEvent::TransactionBegun { + transaction_id: tx_id, + }); + Some(tx_id) + } else { + None } + } - self.display_map.update(cx, update); + pub fn end_transaction_at( + &mut self, + now: Instant, + cx: &mut Context, + ) -> Option { + if let Some(transaction_id) = self + .buffer + .update(cx, |buffer, cx| buffer.end_transaction_at(now, cx)) + { + if let Some((_, end_selections)) = + self.selection_history.transaction_mut(transaction_id) + { + *end_selections = Some(self.selections.disjoint_anchors_arc()); + } else { + log::error!("unexpectedly ended a transaction that wasn't started by this editor"); + } - if auto_scroll { - self.request_autoscroll(Autoscroll::fit(), cx); + cx.emit(EditorEvent::Edited { transaction_id }); + Some(transaction_id) + } else { + None } - - cx.notify(); - self.scrollbar_marker_state.dirty = true; - self.active_indent_guides_state.dirty = true; } - pub fn update_renderer_widths( + pub fn modify_transaction_selection_history( &mut self, - widths: impl IntoIterator, - cx: &mut Context, + transaction_id: TransactionId, + modify: impl FnOnce(&mut (Arc<[Selection]>, Option]>>)), ) -> bool { - self.display_map - .update(cx, |map, cx| map.update_fold_widths(widths, cx)) + self.selection_history + .transaction_mut(transaction_id) + .map(modify) + .is_some() } - pub fn default_fold_placeholder(&self, cx: &App) -> FoldPlaceholder { - self.display_map.read(cx).fold_placeholder.clone() + pub fn toggle_focus( + workspace: &mut Workspace, + _: &actions::ToggleFocus, + window: &mut Window, + cx: &mut Context, + ) { + let Some(item) = workspace.recent_active_item_by_type::(cx) else { + return; + }; + workspace.activate_item(&item, true, true, window, cx); } pub fn set_expand_all_diff_hunks(&mut self, cx: &mut App) { @@ -19581,24 +17809,6 @@ impl Editor { self.focused_block.take() } - pub fn insert_creases( - &mut self, - creases: impl IntoIterator>, - cx: &mut Context, - ) -> Vec { - self.display_map - .update(cx, |map, cx| map.insert_creases(creases, cx)) - } - - pub fn remove_creases( - &mut self, - ids: impl IntoIterator, - cx: &mut Context, - ) -> Vec<(CreaseId, Range)> { - self.display_map - .update(cx, |map, cx| map.remove_creases(ids, cx)) - } - pub fn longest_row(&self, cx: &mut App) -> DisplayRow { self.display_map .update(cx, |map, cx| map.snapshot(cx)) @@ -23650,103 +21860,6 @@ impl Editor { self.read_scroll_position_from_db(item_id, workspace_id, window, cx); } - /// Load folds from the file_folds database table by file path. - /// Used when manually opening a file that was previously closed. - fn load_folds_from_db( - &mut self, - workspace_id: WorkspaceId, - file_path: PathBuf, - window: &mut Window, - cx: &mut Context, - ) { - 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 { - 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 lsp_data_enabled(&self) -> bool { self.enable_lsp_data && self.mode().is_full() } @@ -25215,87 +23328,6 @@ impl EditorSnapshot { } } - pub fn render_crease_toggle( - &self, - buffer_row: MultiBufferRow, - row_contains_cursor: bool, - editor: Entity, - window: &mut Window, - cx: &mut App, - ) -> Option { - 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 { - 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 - } - } - pub fn max_line_number_width(&self, style: &EditorStyle, window: &mut Window) -> Pixels { let digit_count = self.widest_line_number().ilog10() + 1; column_pixels(style, digit_count as usize, window) diff --git a/crates/editor/src/fold.rs b/crates/editor/src/fold.rs new file mode 100644 index 0000000000000000000000000000000000000000..1367505b1d0e776114ce5b3dc244775582cee93a --- /dev/null +++ b/crates/editor/src/fold.rs @@ -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, + window: &mut Window, + cx: &mut App, + ) -> Option { + 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 { + 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, + ) { + 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::(&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, + ) { + let selection = self.selections.newest::(&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) { + 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::>(); + 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, + ) { + 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.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.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.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.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.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.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.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.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.fold_at_level(&actions::FoldAtLevel(9), window, cx); + } + + pub fn fold_all(&mut self, _: &actions::FoldAll, window: &mut Window, cx: &mut Context) { + 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, + ) { + 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::>(); + + 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, + ) { + 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, + ) { + 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::(&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) { + 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::(&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::>(); + + 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::>(); + 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, + ) { + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let selections = self.selections.all::(&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::>(); + + self.unfold_ranges(&ranges, true, true, cx); + } + + pub fn unfold_at( + &mut self, + buffer_row: MultiBufferRow, + _window: &mut Window, + cx: &mut Context, + ) { + 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::(&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, + ) { + 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, + ) { + 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::>(); + self.fold_creases(ranges, true, window, cx); + } + + pub fn fold_ranges( + &mut self, + ranges: Vec>, + auto_scroll: bool, + window: &mut Window, + cx: &mut Context, + ) { + 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::>(); + self.fold_creases(ranges, auto_scroll, window, cx); + } + + pub fn fold_creases( + &mut self, + creases: Vec>, + auto_scroll: bool, + window: &mut Window, + cx: &mut Context, + ) { + 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( + &mut self, + ranges: &[Range], + inclusive: bool, + auto_scroll: bool, + cx: &mut Context, + ) { + 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.fold_buffers([buffer_id], cx); + } + + pub fn fold_buffers( + &mut self, + buffer_ids: impl IntoIterator, + cx: &mut Context, + ) { + if self.buffer().read(cx).is_singleton() { + return; + } + + let ids_to_fold: Vec = 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) { + 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 { + self.display_map.read(cx).folded_buffers() + } + + pub fn disable_header_for_buffer(&mut self, buffer_id: BufferId, cx: &mut Context) { + 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( + &mut self, + ranges: &[Range], + type_id: TypeId, + auto_scroll: bool, + cx: &mut Context, + ) { + 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, + cx: &mut Context, + ) -> 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>, + cx: &mut Context, + ) -> Vec { + self.display_map + .update(cx, |map, cx| map.insert_creases(creases, cx)) + } + + pub fn remove_creases( + &mut self, + ids: impl IntoIterator, + cx: &mut Context, + ) -> Vec<(CreaseId, Range)> { + 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, + ) { + 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> = self + .selections + .all::(&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) { + if self.buffer().read(cx).is_singleton() { + return; + } + let snapshot = self.buffer.read(cx).snapshot(cx); + let buffer_ids: HashSet = 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) { + 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::::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::>(); + 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, + ) { + struct NewlineFold; + let type_id = std::any::TypeId::of::(); + 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::>(); + 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::>(); + + (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, + ) { + 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 { + 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( + &mut self, + ranges: &[Range], + auto_scroll: bool, + cx: &mut Context, + update: impl FnOnce(&mut DisplayMap, &mut Context), + ) { + 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; + } +} diff --git a/crates/editor/src/selection.rs b/crates/editor/src/selection.rs new file mode 100644 index 0000000000000000000000000000000000000000..9918383fb48465b4e1c1cf5b28095a7b69f2a0d3 --- /dev/null +++ b/crates/editor/src/selection.rs @@ -0,0 +1,899 @@ +use super::*; + +impl Editor { + pub fn sync_selections( + &mut self, + other: Entity, + cx: &mut Context, + ) -> 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::(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( + &mut self, + effects: SelectionEffects, + window: &mut Window, + cx: &mut Context, + 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( + &mut self, + window: &mut Window, + cx: &mut Context, + update: impl FnOnce(&mut Self, &mut Window, &mut Context) -> 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, cx: &mut Context) -> 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::(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>, + pending_selection: Option>, + window: &mut Window, + cx: &mut Context, + ) { + 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) { + 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.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.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, + ) { + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let tail = self + .selections + .newest::(&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, + ) { + 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 = { + let selected_points: Vec> = + 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, + ) { + 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::(&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, + window: &mut Window, + cx: &mut Context, + ) { + 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.columnar_selection_state.take(); + if let Some(pending_mode) = self.selections.pending_mode() { + let selections = self + .selections + .all::(&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.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::(&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::(); + + 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::(&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, + ) { + 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, + ) { + 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::>(); + 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, SelectMode)> { + Some(( + self.selections.pending_anchor()?.clone(), + self.selections.pending_mode()?, + )) + } +}