# Objective Both https://github.com/zed-industries/zed/pull/59005 and https://github.com/zed-industries/zed/pull/53789 introduced actions, at the editor context, which allow to select the content between brackets/delimiters, with the latter also adding an action to include the brackets/delimiters too. Shipping both would mean we end up with three distinct, but similar actions: * `editor: select inside enclosing bracket` * `editor: select inside delimiters` * `editor: select around delimiters` As such, in order to simplify this, the changes in this Pull Request remove the first action, while updating the logic for both `editor: select inside delimiters` and `editor: select around delimiters` in order to support continuous outwards expansion, like `editor: select inside enclosing bracket` supported. ## Solution * Remove `editor::SelectInsideEnclosingBracket` action, it's related method and tests * Updated `editor::selection::Editor::select_delimiters_impl` to use `MultiBufferSnapshot::enclosing_bracket_ranges` instead of `MultiBufferSnapshot::innermost_enclosing_bracket_ranges` so we can traverse all bracket ranges and expand to the next one ## Testing Besides manual testing, a new test case was added specifically for the selection expansion support, namely `editor::editor_tests::test_select_delimiters_expansion`. ## Self-Review Checklist: - [x] I've reviewed my own diff for quality, security, and reliability - [x] Unsafe blocks (if any) have justifying comments - [x] The content adheres to Zed's UI standards ([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) and [icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md) guidelines) - [x] Tests cover the new/changed behavior - [x] Performance impact has been considered and is acceptable --- Release Notes: - N/A
2399 lines
91 KiB
Rust
2399 lines
91 KiB
Rust
use super::*;
|
|
|
|
impl Editor {
|
|
pub fn sync_selections(
|
|
&mut self,
|
|
other: Entity<Editor>,
|
|
cx: &mut Context<Self>,
|
|
) -> gpui::Subscription {
|
|
let other_selections = other.read(cx).selections.disjoint_anchors().to_vec();
|
|
if !other_selections.is_empty() {
|
|
self.selections
|
|
.change_with(&self.display_snapshot(cx), |selections| {
|
|
selections.select_anchors(other_selections);
|
|
});
|
|
}
|
|
|
|
let other_subscription = cx.subscribe(&other, |this, other, other_evt, cx| {
|
|
if let EditorEvent::SelectionsChanged { local: true } = other_evt {
|
|
let other_selections = other.read(cx).selections.disjoint_anchors().to_vec();
|
|
if other_selections.is_empty() {
|
|
return;
|
|
}
|
|
let snapshot = this.display_snapshot(cx);
|
|
this.selections.change_with(&snapshot, |selections| {
|
|
selections.select_anchors(other_selections);
|
|
});
|
|
}
|
|
});
|
|
|
|
let this_subscription = cx.subscribe_self::<EditorEvent>(move |this, this_evt, cx| {
|
|
if let EditorEvent::SelectionsChanged { local: true } = this_evt {
|
|
let these_selections = this.selections.disjoint_anchors().to_vec();
|
|
if these_selections.is_empty() {
|
|
return;
|
|
}
|
|
other.update(cx, |other_editor, cx| {
|
|
let snapshot = other_editor.display_snapshot(cx);
|
|
other_editor
|
|
.selections
|
|
.change_with(&snapshot, |selections| {
|
|
selections.select_anchors(these_selections);
|
|
})
|
|
});
|
|
}
|
|
});
|
|
|
|
Subscription::join(other_subscription, this_subscription)
|
|
}
|
|
|
|
/// Changes selections using the provided mutation function. Changes to `self.selections` occur
|
|
/// immediately, but when run within `transact` or `with_selection_effects_deferred` other
|
|
/// effects of selection change occur at the end of the transaction.
|
|
pub fn change_selections<R>(
|
|
&mut self,
|
|
effects: SelectionEffects,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
change: impl FnOnce(&mut MutableSelectionsCollection<'_, '_>) -> R,
|
|
) -> R {
|
|
let snapshot = self.display_snapshot(cx);
|
|
if let Some(state) = &mut self.deferred_selection_effects_state {
|
|
state.effects.scroll = effects.scroll.or(state.effects.scroll);
|
|
state.effects.completions = effects.completions;
|
|
state.effects.nav_history = effects.nav_history.or(state.effects.nav_history);
|
|
let (changed, result) = self.selections.change_with(&snapshot, change);
|
|
state.changed |= changed;
|
|
return result;
|
|
}
|
|
let mut state = DeferredSelectionEffectsState {
|
|
changed: false,
|
|
effects,
|
|
old_cursor_position: self.selections.newest_anchor().head(),
|
|
history_entry: SelectionHistoryEntry {
|
|
selections: self.selections.disjoint_anchors_arc(),
|
|
select_next_state: self.select_next_state.clone(),
|
|
select_prev_state: self.select_prev_state.clone(),
|
|
add_selections_state: self.add_selections_state.clone(),
|
|
},
|
|
};
|
|
let (changed, result) = self.selections.change_with(&snapshot, change);
|
|
state.changed = state.changed || changed;
|
|
if self.defer_selection_effects {
|
|
self.deferred_selection_effects_state = Some(state);
|
|
} else {
|
|
self.apply_selection_effects(state, window, cx);
|
|
}
|
|
result
|
|
}
|
|
|
|
/// Defers the effects of selection change, so that the effects of multiple calls to
|
|
/// `change_selections` are applied at the end. This way these intermediate states aren't added
|
|
/// to selection history and the state of popovers based on selection position aren't
|
|
/// erroneously updated.
|
|
pub fn with_selection_effects_deferred<R>(
|
|
&mut self,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
update: impl FnOnce(&mut Self, &mut Window, &mut Context<Self>) -> R,
|
|
) -> R {
|
|
let already_deferred = self.defer_selection_effects;
|
|
self.defer_selection_effects = true;
|
|
let result = update(self, window, cx);
|
|
if !already_deferred {
|
|
self.defer_selection_effects = false;
|
|
if let Some(state) = self.deferred_selection_effects_state.take() {
|
|
self.apply_selection_effects(state, window, cx);
|
|
}
|
|
}
|
|
result
|
|
}
|
|
|
|
pub fn has_non_empty_selection(&self, snapshot: &DisplaySnapshot) -> bool {
|
|
self.selections
|
|
.all_adjusted(snapshot)
|
|
.iter()
|
|
.any(|selection| !selection.is_empty())
|
|
}
|
|
|
|
pub fn is_range_selected(&mut self, range: &Range<Anchor>, cx: &mut Context<Self>) -> bool {
|
|
if self
|
|
.selections
|
|
.pending_anchor()
|
|
.is_some_and(|pending_selection| {
|
|
let snapshot = self.buffer().read(cx).snapshot(cx);
|
|
pending_selection.range().includes(range, &snapshot)
|
|
})
|
|
{
|
|
return true;
|
|
}
|
|
|
|
self.selections
|
|
.disjoint_in_range::<MultiBufferOffset>(range.clone(), &self.display_snapshot(cx))
|
|
.into_iter()
|
|
.any(|selection| {
|
|
// This is needed to cover a corner case, if we just check for an existing
|
|
// selection in the fold range, having a cursor at the start of the fold
|
|
// marks it as selected. Non-empty selections don't cause this.
|
|
let length = selection.end - selection.start;
|
|
length > 0
|
|
})
|
|
}
|
|
|
|
pub fn has_pending_nonempty_selection(&self) -> bool {
|
|
let pending_nonempty_selection = match self.selections.pending_anchor() {
|
|
Some(Selection { start, end, .. }) => start != end,
|
|
None => false,
|
|
};
|
|
|
|
pending_nonempty_selection
|
|
|| (self.columnar_selection_state.is_some()
|
|
&& self.selections.disjoint_anchors().len() > 1)
|
|
}
|
|
|
|
pub fn has_pending_selection(&self) -> bool {
|
|
self.selections.pending_anchor().is_some() || self.columnar_selection_state.is_some()
|
|
}
|
|
|
|
pub fn set_selections_from_remote(
|
|
&mut self,
|
|
selections: Vec<Selection<Anchor>>,
|
|
pending_selection: Option<Selection<Anchor>>,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
let old_cursor_position = self.selections.newest_anchor().head();
|
|
self.selections
|
|
.change_with(&self.display_snapshot(cx), |s| {
|
|
s.select_anchors(selections);
|
|
if let Some(pending_selection) = pending_selection {
|
|
s.set_pending(pending_selection, SelectMode::Character);
|
|
} else {
|
|
s.clear_pending();
|
|
}
|
|
});
|
|
self.selections_did_change(
|
|
false,
|
|
&old_cursor_position,
|
|
SelectionEffects::default(),
|
|
window,
|
|
cx,
|
|
);
|
|
}
|
|
|
|
pub fn set_mark(&mut self, _: &actions::SetMark, window: &mut Window, cx: &mut Context<Self>) {
|
|
if self.selection_mark_mode {
|
|
self.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
|
s.move_with(&mut |_, sel| {
|
|
sel.collapse_to(sel.head(), SelectionGoal::None);
|
|
});
|
|
})
|
|
}
|
|
self.selection_mark_mode = true;
|
|
cx.notify();
|
|
}
|
|
|
|
pub fn swap_selection_ends(
|
|
&mut self,
|
|
_: &actions::SwapSelectionEnds,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
self.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
|
s.move_with(&mut |_, sel| {
|
|
if sel.start != sel.end {
|
|
sel.reversed = !sel.reversed
|
|
}
|
|
});
|
|
});
|
|
self.request_autoscroll(Autoscroll::newest(), cx);
|
|
cx.notify();
|
|
}
|
|
|
|
pub fn select_to_end(&mut self, _: &SelectToEnd, window: &mut Window, cx: &mut Context<Self>) {
|
|
let buffer = self.buffer.read(cx).snapshot(cx);
|
|
let mut selection = self
|
|
.selections
|
|
.first::<MultiBufferOffset>(&self.display_snapshot(cx));
|
|
selection.set_head(buffer.len(), SelectionGoal::None);
|
|
self.change_selections(Default::default(), window, cx, |s| {
|
|
s.select(vec![selection]);
|
|
});
|
|
}
|
|
|
|
pub fn select_all(&mut self, _: &SelectAll, window: &mut Window, cx: &mut Context<Self>) {
|
|
self.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
|
s.select_ranges(vec![Anchor::Min..Anchor::Max]);
|
|
});
|
|
}
|
|
|
|
pub fn select_line(&mut self, _: &SelectLine, window: &mut Window, cx: &mut Context<Self>) {
|
|
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
|
|
let mut selections = self.selections.all::<Point>(&display_map);
|
|
let max_point = display_map.buffer_snapshot().max_point();
|
|
for selection in &mut selections {
|
|
let rows = selection.spanned_rows(true, &display_map);
|
|
selection.start = Point::new(rows.start.0, 0);
|
|
selection.end = cmp::min(max_point, Point::new(rows.end.0, 0));
|
|
selection.reversed = false;
|
|
}
|
|
self.change_selections(Default::default(), window, cx, |s| {
|
|
s.select(selections);
|
|
});
|
|
}
|
|
|
|
pub fn split_selection_into_lines(
|
|
&mut self,
|
|
action: &SplitSelectionIntoLines,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
let selections = self
|
|
.selections
|
|
.all::<Point>(&self.display_snapshot(cx))
|
|
.into_iter()
|
|
.map(|selection| selection.start..selection.end)
|
|
.collect::<Vec<_>>();
|
|
self.unfold_ranges(&selections, true, false, cx);
|
|
|
|
let mut new_selection_ranges = Vec::new();
|
|
{
|
|
let buffer = self.buffer.read(cx).read(cx);
|
|
for selection in selections {
|
|
for row in selection.start.row..selection.end.row {
|
|
let line_start = Point::new(row, 0);
|
|
let line_end = Point::new(row, buffer.line_len(MultiBufferRow(row)));
|
|
|
|
if action.keep_selections {
|
|
// Keep the selection range for each line
|
|
let selection_start = if row == selection.start.row {
|
|
selection.start
|
|
} else {
|
|
line_start
|
|
};
|
|
new_selection_ranges.push(selection_start..line_end);
|
|
} else {
|
|
// Collapse to cursor at end of line
|
|
new_selection_ranges.push(line_end..line_end);
|
|
}
|
|
}
|
|
|
|
let is_multiline_selection = selection.start.row != selection.end.row;
|
|
// Don't insert last one if it's a multi-line selection ending at the start of a line,
|
|
// so this action feels more ergonomic when paired with other selection operations
|
|
let should_skip_last = is_multiline_selection && selection.end.column == 0;
|
|
if !should_skip_last {
|
|
if action.keep_selections {
|
|
if is_multiline_selection {
|
|
let line_start = Point::new(selection.end.row, 0);
|
|
new_selection_ranges.push(line_start..selection.end);
|
|
} else {
|
|
new_selection_ranges.push(selection.start..selection.end);
|
|
}
|
|
} else {
|
|
new_selection_ranges.push(selection.end..selection.end);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
self.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
|
s.select_ranges(new_selection_ranges);
|
|
});
|
|
}
|
|
|
|
pub fn add_selection_above(
|
|
&mut self,
|
|
action: &AddSelectionAbove,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
self.add_selection(true, action.skip_soft_wrap, window, cx);
|
|
}
|
|
|
|
pub fn add_selection_below(
|
|
&mut self,
|
|
action: &AddSelectionBelow,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
self.add_selection(false, action.skip_soft_wrap, window, cx);
|
|
}
|
|
|
|
pub fn select_all_matches(
|
|
&mut self,
|
|
_action: &SelectAllMatches,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) -> Result<()> {
|
|
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
|
|
|
|
self.select_next_match_internal(&display_map, false, None, window, cx)?;
|
|
let Some(select_next_state) = self.select_next_state.as_mut().filter(|state| !state.done)
|
|
else {
|
|
return Ok(());
|
|
};
|
|
|
|
let mut new_selections = Vec::new();
|
|
let initial_selection = self.selections.oldest::<MultiBufferOffset>(&display_map);
|
|
let reversed = initial_selection.reversed;
|
|
let buffer = display_map.buffer_snapshot();
|
|
let query_matches = select_next_state
|
|
.query
|
|
.stream_find_iter(buffer.bytes_in_range(MultiBufferOffset(0)..buffer.len()));
|
|
|
|
for query_match in query_matches.into_iter() {
|
|
let query_match = query_match.context("query match for select all action")?; // can only fail due to I/O
|
|
let offset_range = if reversed {
|
|
MultiBufferOffset(query_match.end())..MultiBufferOffset(query_match.start())
|
|
} else {
|
|
MultiBufferOffset(query_match.start())..MultiBufferOffset(query_match.end())
|
|
};
|
|
|
|
let is_partial_word_match = select_next_state.wordwise
|
|
&& (buffer.is_inside_word(offset_range.start, None)
|
|
|| buffer.is_inside_word(offset_range.end, None));
|
|
|
|
let is_initial_selection = MultiBufferOffset(query_match.start())
|
|
== initial_selection.start
|
|
&& MultiBufferOffset(query_match.end()) == initial_selection.end;
|
|
|
|
if !is_partial_word_match && !is_initial_selection {
|
|
new_selections.push(offset_range);
|
|
}
|
|
}
|
|
|
|
// Ensure that the initial range is the last selection, as
|
|
// `MutableSelectionsCollection::select_ranges` makes the last selection
|
|
// the newest selection, which the editor then relies on as the primary
|
|
// cursor for scroll targeting. Without this, the last match would then
|
|
// be automatically focused when the user started editing the selected
|
|
// matches.
|
|
let initial_directed_range = if reversed {
|
|
initial_selection.end..initial_selection.start
|
|
} else {
|
|
initial_selection.start..initial_selection.end
|
|
};
|
|
new_selections.push(initial_directed_range);
|
|
|
|
select_next_state.done = true;
|
|
self.unfold_ranges(&new_selections, false, false, cx);
|
|
self.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
|
|
selections.select_ranges(new_selections)
|
|
});
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub fn select_next(
|
|
&mut self,
|
|
action: &SelectNext,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) -> Result<()> {
|
|
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
|
|
self.select_next_match_internal(
|
|
&display_map,
|
|
action.replace_newest,
|
|
Some(Autoscroll::newest()),
|
|
window,
|
|
cx,
|
|
)
|
|
}
|
|
|
|
pub fn select_previous(
|
|
&mut self,
|
|
action: &SelectPrevious,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) -> Result<()> {
|
|
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
|
|
let buffer = display_map.buffer_snapshot();
|
|
let mut selections = self.selections.all::<MultiBufferOffset>(&display_map);
|
|
if let Some(mut select_prev_state) = self.select_prev_state.take() {
|
|
let query = &select_prev_state.query;
|
|
if !select_prev_state.done {
|
|
let first_selection = selections
|
|
.iter()
|
|
.min_by_key(|s| s.id)
|
|
.context("missing selection for select previous action")?;
|
|
let last_selection = selections
|
|
.iter()
|
|
.max_by_key(|s| s.id)
|
|
.context("missing selection for select previous action")?;
|
|
let mut next_selected_range = None;
|
|
// When we're iterating matches backwards, the oldest match will actually be the furthest one in the buffer.
|
|
let bytes_before_last_selection =
|
|
buffer.reversed_bytes_in_range(MultiBufferOffset(0)..last_selection.start);
|
|
let bytes_after_first_selection =
|
|
buffer.reversed_bytes_in_range(first_selection.end..buffer.len());
|
|
let query_matches = query
|
|
.stream_find_iter(bytes_before_last_selection)
|
|
.map(|result| (last_selection.start, result))
|
|
.chain(
|
|
query
|
|
.stream_find_iter(bytes_after_first_selection)
|
|
.map(|result| (buffer.len(), result)),
|
|
);
|
|
for (end_offset, query_match) in query_matches {
|
|
let query_match =
|
|
query_match.context("query match for select previous action")?;
|
|
let offset_range =
|
|
end_offset - query_match.end()..end_offset - query_match.start();
|
|
|
|
if !select_prev_state.wordwise
|
|
|| (!buffer.is_inside_word(offset_range.start, None)
|
|
&& !buffer.is_inside_word(offset_range.end, None))
|
|
{
|
|
next_selected_range = Some(offset_range);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if let Some(next_selected_range) = next_selected_range {
|
|
self.select_match_ranges(
|
|
next_selected_range,
|
|
last_selection.reversed,
|
|
action.replace_newest,
|
|
Some(Autoscroll::newest()),
|
|
window,
|
|
cx,
|
|
);
|
|
} else {
|
|
select_prev_state.done = true;
|
|
}
|
|
}
|
|
|
|
self.select_prev_state = Some(select_prev_state);
|
|
} else {
|
|
let mut only_carets = true;
|
|
let mut same_text_selected = true;
|
|
let mut selected_text = None;
|
|
|
|
let mut selections_iter = selections.iter().peekable();
|
|
while let Some(selection) = selections_iter.next() {
|
|
if selection.start != selection.end {
|
|
only_carets = false;
|
|
}
|
|
|
|
if same_text_selected {
|
|
if selected_text.is_none() {
|
|
selected_text =
|
|
Some(buffer.text_for_range(selection.range()).collect::<String>());
|
|
}
|
|
|
|
if let Some(next_selection) = selections_iter.peek() {
|
|
if next_selection.len() == selection.len() {
|
|
let next_selected_text = buffer
|
|
.text_for_range(next_selection.range())
|
|
.collect::<String>();
|
|
if Some(next_selected_text) != selected_text {
|
|
same_text_selected = false;
|
|
selected_text = None;
|
|
}
|
|
} else {
|
|
same_text_selected = false;
|
|
selected_text = None;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if only_carets {
|
|
for selection in &mut selections {
|
|
let (word_range, _) = buffer.surrounding_word(selection.start, None);
|
|
selection.start = word_range.start;
|
|
selection.end = word_range.end;
|
|
selection.goal = SelectionGoal::None;
|
|
selection.reversed = false;
|
|
self.select_match_ranges(
|
|
selection.start..selection.end,
|
|
selection.reversed,
|
|
action.replace_newest,
|
|
Some(Autoscroll::newest()),
|
|
window,
|
|
cx,
|
|
);
|
|
}
|
|
if selections.len() == 1 {
|
|
let selection = selections
|
|
.last()
|
|
.expect("ensured that there's only one selection");
|
|
let query = buffer
|
|
.text_for_range(selection.start..selection.end)
|
|
.collect::<String>();
|
|
let is_empty = query.is_empty();
|
|
let select_state = SelectNextState {
|
|
query: self.build_query(&[query.chars().rev().collect::<String>()], cx)?,
|
|
wordwise: true,
|
|
done: is_empty,
|
|
};
|
|
self.select_prev_state = Some(select_state);
|
|
} else {
|
|
self.select_prev_state = None;
|
|
}
|
|
} else if let Some(selected_text) = selected_text {
|
|
self.select_prev_state = Some(SelectNextState {
|
|
query: self
|
|
.build_query(&[selected_text.chars().rev().collect::<String>()], cx)?,
|
|
wordwise: false,
|
|
done: false,
|
|
});
|
|
self.select_previous(action, window, cx)?;
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub fn find_next_match(
|
|
&mut self,
|
|
_: &FindNextMatch,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) -> Result<()> {
|
|
let selections = self.selections.disjoint_anchors_arc();
|
|
match selections.first() {
|
|
Some(first) if selections.len() >= 2 => {
|
|
self.change_selections(Default::default(), window, cx, |s| {
|
|
s.select_ranges([first.range()]);
|
|
});
|
|
}
|
|
_ => self.select_next(
|
|
&SelectNext {
|
|
replace_newest: true,
|
|
},
|
|
window,
|
|
cx,
|
|
)?,
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub fn find_previous_match(
|
|
&mut self,
|
|
_: &FindPreviousMatch,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) -> Result<()> {
|
|
let selections = self.selections.disjoint_anchors_arc();
|
|
match selections.last() {
|
|
Some(last) if selections.len() >= 2 => {
|
|
self.change_selections(Default::default(), window, cx, |s| {
|
|
s.select_ranges([last.range()]);
|
|
});
|
|
}
|
|
_ => self.select_previous(
|
|
&SelectPrevious {
|
|
replace_newest: true,
|
|
},
|
|
window,
|
|
cx,
|
|
)?,
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub fn select_enclosing_symbol(
|
|
&mut self,
|
|
_: &SelectEnclosingSymbol,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
let buffer = self.buffer.read(cx).snapshot(cx);
|
|
let old_selections = self
|
|
.selections
|
|
.all::<MultiBufferOffset>(&self.display_snapshot(cx))
|
|
.into_boxed_slice();
|
|
|
|
fn update_selection(
|
|
selection: &Selection<MultiBufferOffset>,
|
|
buffer_snap: &MultiBufferSnapshot,
|
|
) -> Option<Selection<MultiBufferOffset>> {
|
|
let cursor = selection.head();
|
|
let (_buffer_id, symbols) = buffer_snap.symbols_containing(cursor, None)?;
|
|
for symbol in symbols.iter().rev() {
|
|
let start = symbol.range.start.to_offset(buffer_snap);
|
|
let end = symbol.range.end.to_offset(buffer_snap);
|
|
let new_range = start..end;
|
|
if start < selection.start || end > selection.end {
|
|
return Some(Selection {
|
|
id: selection.id,
|
|
start: new_range.start,
|
|
end: new_range.end,
|
|
goal: SelectionGoal::None,
|
|
reversed: selection.reversed,
|
|
});
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
let mut selected_larger_symbol = false;
|
|
let new_selections = old_selections
|
|
.iter()
|
|
.map(|selection| match update_selection(selection, &buffer) {
|
|
Some(new_selection) => {
|
|
if new_selection.range() != selection.range() {
|
|
selected_larger_symbol = true;
|
|
}
|
|
new_selection
|
|
}
|
|
None => selection.clone(),
|
|
})
|
|
.collect::<Vec<_>>();
|
|
|
|
if selected_larger_symbol {
|
|
self.change_selections(Default::default(), window, cx, |s| {
|
|
s.select(new_selections);
|
|
});
|
|
}
|
|
}
|
|
|
|
pub fn select_larger_syntax_node(
|
|
&mut self,
|
|
_: &SelectLargerSyntaxNode,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
let Some(visible_row_count) = self.visible_row_count() else {
|
|
return;
|
|
};
|
|
let old_selections: Box<[_]> = self
|
|
.selections
|
|
.all::<MultiBufferOffset>(&self.display_snapshot(cx))
|
|
.into();
|
|
if old_selections.is_empty() {
|
|
return;
|
|
}
|
|
|
|
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
|
|
let buffer = self.buffer.read(cx).snapshot(cx);
|
|
|
|
let mut selected_larger_node = false;
|
|
let mut new_selections = old_selections
|
|
.iter()
|
|
.map(|selection| {
|
|
let old_range = selection.start..selection.end;
|
|
|
|
if let Some((node, _)) = buffer.syntax_ancestor(old_range.clone()) {
|
|
// manually select word at selection
|
|
if ["string_content", "inline"].contains(&node.kind()) {
|
|
let (word_range, _) = buffer.surrounding_word(old_range.start, None);
|
|
// ignore if word is already selected
|
|
if !word_range.is_empty() && old_range != word_range {
|
|
let (last_word_range, _) = buffer.surrounding_word(old_range.end, None);
|
|
// only select word if start and end point belongs to same word
|
|
if word_range == last_word_range {
|
|
selected_larger_node = true;
|
|
return Selection {
|
|
id: selection.id,
|
|
start: word_range.start,
|
|
end: word_range.end,
|
|
goal: SelectionGoal::None,
|
|
reversed: selection.reversed,
|
|
};
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
let mut new_range = old_range.clone();
|
|
while let Some((node, range)) = buffer.syntax_ancestor(new_range.clone()) {
|
|
new_range = range;
|
|
if !node.is_named() {
|
|
continue;
|
|
}
|
|
if !display_map.intersects_fold(new_range.start)
|
|
&& !display_map.intersects_fold(new_range.end)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
selected_larger_node |= new_range != old_range;
|
|
Selection {
|
|
id: selection.id,
|
|
start: new_range.start,
|
|
end: new_range.end,
|
|
goal: SelectionGoal::None,
|
|
reversed: selection.reversed,
|
|
}
|
|
})
|
|
.collect::<Vec<_>>();
|
|
|
|
if !selected_larger_node {
|
|
return; // don't put this call in the history
|
|
}
|
|
|
|
// scroll based on transformation done to the last selection created by the user
|
|
let (last_old, last_new) = old_selections
|
|
.last()
|
|
.zip(new_selections.last().cloned())
|
|
.expect("old_selections isn't empty");
|
|
|
|
let is_selection_reversed = if new_selections.len() == 1 {
|
|
let should_be_reversed = last_old.start != last_new.start;
|
|
new_selections.last_mut().expect("checked above").reversed = should_be_reversed;
|
|
should_be_reversed
|
|
} else {
|
|
last_new.reversed
|
|
};
|
|
|
|
self.select_syntax_node_history.disable_clearing = true;
|
|
self.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
|
s.select(new_selections.clone());
|
|
});
|
|
self.select_syntax_node_history.disable_clearing = false;
|
|
|
|
let start_row = last_new.start.to_display_point(&display_map).row().0;
|
|
let end_row = last_new.end.to_display_point(&display_map).row().0;
|
|
let selection_height = end_row - start_row + 1;
|
|
let scroll_margin_rows = self.vertical_scroll_margin() as u32;
|
|
|
|
let fits_on_the_screen = visible_row_count >= selection_height + scroll_margin_rows * 2;
|
|
let scroll_behavior = if fits_on_the_screen {
|
|
self.request_autoscroll(Autoscroll::fit(), cx);
|
|
SelectSyntaxNodeScrollBehavior::FitSelection
|
|
} else if is_selection_reversed {
|
|
self.scroll_cursor_top(&ScrollCursorTop, window, cx);
|
|
SelectSyntaxNodeScrollBehavior::CursorTop
|
|
} else {
|
|
self.scroll_cursor_bottom(&ScrollCursorBottom, window, cx);
|
|
SelectSyntaxNodeScrollBehavior::CursorBottom
|
|
};
|
|
|
|
let old_selections: Box<[Selection<Anchor>]> = old_selections
|
|
.iter()
|
|
.map(|s| s.map(|offset| buffer.anchor_before(offset)))
|
|
.collect();
|
|
self.select_syntax_node_history.push((
|
|
old_selections,
|
|
scroll_behavior,
|
|
is_selection_reversed,
|
|
));
|
|
}
|
|
|
|
pub fn select_smaller_syntax_node(
|
|
&mut self,
|
|
_: &SelectSmallerSyntaxNode,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
if let Some((mut selections, scroll_behavior, is_selection_reversed)) =
|
|
self.select_syntax_node_history.pop()
|
|
{
|
|
if let Some(selection) = selections.last_mut() {
|
|
selection.reversed = is_selection_reversed;
|
|
}
|
|
|
|
let snapshot = self.buffer.read(cx).snapshot(cx);
|
|
let selections: Vec<Selection<MultiBufferOffset>> = selections
|
|
.iter()
|
|
.map(|s| s.map(|anchor| anchor.to_offset(&snapshot)))
|
|
.collect();
|
|
|
|
self.select_syntax_node_history.disable_clearing = true;
|
|
self.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
|
s.select(selections);
|
|
});
|
|
self.select_syntax_node_history.disable_clearing = false;
|
|
|
|
match scroll_behavior {
|
|
SelectSyntaxNodeScrollBehavior::CursorTop => {
|
|
self.scroll_cursor_top(&ScrollCursorTop, window, cx);
|
|
}
|
|
SelectSyntaxNodeScrollBehavior::FitSelection => {
|
|
self.request_autoscroll(Autoscroll::fit(), cx);
|
|
}
|
|
SelectSyntaxNodeScrollBehavior::CursorBottom => {
|
|
self.scroll_cursor_bottom(&ScrollCursorBottom, window, cx);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn select_next_syntax_node(
|
|
&mut self,
|
|
_: &SelectNextSyntaxNode,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
let old_selections = self.selections.all_anchors(&self.display_snapshot(cx));
|
|
if old_selections.is_empty() {
|
|
return;
|
|
}
|
|
|
|
let buffer = self.buffer.read(cx).snapshot(cx);
|
|
let mut selected_sibling = false;
|
|
|
|
let new_selections = old_selections
|
|
.iter()
|
|
.map(|selection| {
|
|
let old_range =
|
|
selection.start.to_offset(&buffer)..selection.end.to_offset(&buffer);
|
|
if let Some(results) = buffer.map_excerpt_ranges(
|
|
old_range,
|
|
|buf, _excerpt_range, input_buffer_range| {
|
|
let Some(node) = buf.syntax_next_sibling(input_buffer_range) else {
|
|
return Vec::new();
|
|
};
|
|
vec![(
|
|
BufferOffset(node.byte_range().start)
|
|
..BufferOffset(node.byte_range().end),
|
|
(),
|
|
)]
|
|
},
|
|
) && let [(new_range, _)] = results.as_slice()
|
|
{
|
|
selected_sibling = true;
|
|
let new_range =
|
|
buffer.anchor_after(new_range.start)..buffer.anchor_before(new_range.end);
|
|
Selection {
|
|
id: selection.id,
|
|
start: new_range.start,
|
|
end: new_range.end,
|
|
goal: SelectionGoal::None,
|
|
reversed: selection.reversed,
|
|
}
|
|
} else {
|
|
selection.clone()
|
|
}
|
|
})
|
|
.collect::<Vec<_>>();
|
|
|
|
if selected_sibling {
|
|
self.change_selections(
|
|
SelectionEffects::scroll(Autoscroll::fit()),
|
|
window,
|
|
cx,
|
|
|s| {
|
|
s.select(new_selections);
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
pub fn select_prev_syntax_node(
|
|
&mut self,
|
|
_: &SelectPreviousSyntaxNode,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
let old_selections: Arc<[_]> = self.selections.all_anchors(&self.display_snapshot(cx));
|
|
|
|
let multibuffer_snapshot = self.buffer.read(cx).snapshot(cx);
|
|
let mut selected_sibling = false;
|
|
|
|
let new_selections = old_selections
|
|
.iter()
|
|
.map(|selection| {
|
|
let old_range = selection.start.to_offset(&multibuffer_snapshot)
|
|
..selection.end.to_offset(&multibuffer_snapshot);
|
|
if let Some(results) = multibuffer_snapshot.map_excerpt_ranges(
|
|
old_range,
|
|
|buf, _excerpt_range, input_buffer_range| {
|
|
let Some(node) = buf.syntax_prev_sibling(input_buffer_range) else {
|
|
return Vec::new();
|
|
};
|
|
vec![(
|
|
BufferOffset(node.byte_range().start)
|
|
..BufferOffset(node.byte_range().end),
|
|
(),
|
|
)]
|
|
},
|
|
) && let [(new_range, _)] = results.as_slice()
|
|
{
|
|
selected_sibling = true;
|
|
let new_range = multibuffer_snapshot.anchor_after(new_range.start)
|
|
..multibuffer_snapshot.anchor_before(new_range.end);
|
|
Selection {
|
|
id: selection.id,
|
|
start: new_range.start,
|
|
end: new_range.end,
|
|
goal: SelectionGoal::None,
|
|
reversed: selection.reversed,
|
|
}
|
|
} else {
|
|
selection.clone()
|
|
}
|
|
})
|
|
.collect::<Vec<_>>();
|
|
|
|
if selected_sibling {
|
|
self.change_selections(
|
|
SelectionEffects::scroll(Autoscroll::fit()),
|
|
window,
|
|
cx,
|
|
|s| {
|
|
s.select(new_selections);
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
pub fn move_to_start_of_larger_syntax_node(
|
|
&mut self,
|
|
_: &MoveToStartOfLargerSyntaxNode,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
self.move_cursors_to_syntax_nodes(window, cx, false);
|
|
}
|
|
|
|
pub fn move_to_end_of_larger_syntax_node(
|
|
&mut self,
|
|
_: &MoveToEndOfLargerSyntaxNode,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
self.move_cursors_to_syntax_nodes(window, cx, true);
|
|
}
|
|
|
|
pub fn select_to_start_of_larger_syntax_node(
|
|
&mut self,
|
|
_: &SelectToStartOfLargerSyntaxNode,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
self.select_to_syntax_nodes(window, cx, false);
|
|
}
|
|
|
|
pub fn select_to_end_of_larger_syntax_node(
|
|
&mut self,
|
|
_: &SelectToEndOfLargerSyntaxNode,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
self.select_to_syntax_nodes(window, cx, true);
|
|
}
|
|
|
|
pub fn select_inside_delimiters(
|
|
&mut self,
|
|
_: &SelectInsideDelimiters,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
self.select_delimiters_impl(false, window, cx);
|
|
}
|
|
|
|
pub fn select_around_delimiters(
|
|
&mut self,
|
|
_: &SelectAroundDelimiters,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
self.select_delimiters_impl(true, window, cx);
|
|
}
|
|
|
|
fn select_delimiters_impl(
|
|
&mut self,
|
|
include_brackets: bool,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
self.change_selections(Default::default(), window, cx, |s| {
|
|
s.move_offsets_with(&mut |snapshot, selection| {
|
|
let Some(enclosing_bracket_ranges) =
|
|
snapshot.enclosing_bracket_ranges(selection.start..selection.end)
|
|
else {
|
|
return;
|
|
};
|
|
|
|
let mut best = None;
|
|
let mut best_length = usize::MAX;
|
|
|
|
for (open, close) in enclosing_bracket_ranges {
|
|
let range = if include_brackets {
|
|
open.start..close.end
|
|
} else {
|
|
open.end..close.start
|
|
};
|
|
|
|
// Skip any bracket pair that is already covered by the
|
|
// selection so repeated uses of delimiters selection only
|
|
// evere expands outwards to the next pair.
|
|
if (selection.start..selection.end).contains_inclusive(&range) {
|
|
continue;
|
|
}
|
|
|
|
let length = close.end - open.start;
|
|
if length < best_length {
|
|
best_length = length;
|
|
best = Some(range);
|
|
}
|
|
}
|
|
|
|
if let Some(range) = best {
|
|
selection.set_head_tail(range.end, range.start, SelectionGoal::None);
|
|
}
|
|
})
|
|
});
|
|
}
|
|
|
|
pub fn move_to_enclosing_bracket(
|
|
&mut self,
|
|
_: &MoveToEnclosingBracket,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
self.change_selections(Default::default(), window, cx, |s| {
|
|
s.move_offsets_with(&mut |snapshot, selection| {
|
|
let Some(enclosing_bracket_ranges) =
|
|
snapshot.enclosing_bracket_ranges(selection.start..selection.end)
|
|
else {
|
|
return;
|
|
};
|
|
|
|
let mut best_length = usize::MAX;
|
|
let mut best_inside = false;
|
|
let mut best_in_bracket_range = false;
|
|
let mut best_destination = None;
|
|
for (open, close) in enclosing_bracket_ranges {
|
|
let close = close.to_inclusive();
|
|
let length = *close.end() - open.start;
|
|
let inside = selection.start >= open.end && selection.end <= *close.start();
|
|
let in_bracket_range = open.to_inclusive().contains(&selection.head())
|
|
|| close.contains(&selection.head());
|
|
|
|
// If best is next to a bracket and current isn't, skip
|
|
if !in_bracket_range && best_in_bracket_range {
|
|
continue;
|
|
}
|
|
|
|
// Prefer smaller lengths unless best is inside and current isn't
|
|
if length > best_length && (best_inside || !inside) {
|
|
continue;
|
|
}
|
|
|
|
best_length = length;
|
|
best_inside = inside;
|
|
best_in_bracket_range = in_bracket_range;
|
|
best_destination = Some(
|
|
if close.contains(&selection.start) && close.contains(&selection.end) {
|
|
if inside { open.end } else { open.start }
|
|
} else if inside {
|
|
*close.start()
|
|
} else {
|
|
*close.end()
|
|
},
|
|
);
|
|
}
|
|
|
|
if let Some(destination) = best_destination {
|
|
selection.collapse_to(destination, SelectionGoal::None);
|
|
}
|
|
})
|
|
});
|
|
}
|
|
|
|
pub fn undo_selection(
|
|
&mut self,
|
|
_: &UndoSelection,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
if let Some(entry) = self.selection_history.undo_stack.pop_back() {
|
|
self.selection_history.mode = SelectionHistoryMode::Undoing;
|
|
self.with_selection_effects_deferred(window, cx, |this, window, cx| {
|
|
this.end_selection(window, cx);
|
|
this.change_selections(
|
|
SelectionEffects::scroll(Autoscroll::newest()),
|
|
window,
|
|
cx,
|
|
|s| s.select_anchors(entry.selections.to_vec()),
|
|
);
|
|
});
|
|
self.selection_history.mode = SelectionHistoryMode::Normal;
|
|
|
|
self.select_next_state = entry.select_next_state;
|
|
self.select_prev_state = entry.select_prev_state;
|
|
self.add_selections_state = entry.add_selections_state;
|
|
}
|
|
}
|
|
|
|
pub fn redo_selection(
|
|
&mut self,
|
|
_: &RedoSelection,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
if let Some(entry) = self.selection_history.redo_stack.pop_back() {
|
|
self.selection_history.mode = SelectionHistoryMode::Redoing;
|
|
self.with_selection_effects_deferred(window, cx, |this, window, cx| {
|
|
this.end_selection(window, cx);
|
|
this.change_selections(
|
|
SelectionEffects::scroll(Autoscroll::newest()),
|
|
window,
|
|
cx,
|
|
|s| s.select_anchors(entry.selections.to_vec()),
|
|
);
|
|
});
|
|
self.selection_history.mode = SelectionHistoryMode::Normal;
|
|
|
|
self.select_next_state = entry.select_next_state;
|
|
self.select_prev_state = entry.select_prev_state;
|
|
self.add_selections_state = entry.add_selections_state;
|
|
}
|
|
}
|
|
|
|
pub(super) fn select(
|
|
&mut self,
|
|
phase: SelectPhase,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
self.hide_context_menu(window, cx);
|
|
|
|
match phase {
|
|
SelectPhase::Begin {
|
|
position,
|
|
add,
|
|
click_count,
|
|
} => self.begin_selection(position, add, click_count, window, cx),
|
|
SelectPhase::BeginColumnar {
|
|
position,
|
|
goal_column,
|
|
reset,
|
|
mode,
|
|
} => self.begin_columnar_selection(position, goal_column, reset, mode, window, cx),
|
|
SelectPhase::Extend {
|
|
position,
|
|
click_count,
|
|
} => self.extend_selection(position, click_count, window, cx),
|
|
SelectPhase::Update {
|
|
position,
|
|
goal_column,
|
|
scroll_delta,
|
|
} => self.update_selection(position, goal_column, scroll_delta, window, cx),
|
|
SelectPhase::End => self.end_selection(window, cx),
|
|
}
|
|
}
|
|
|
|
pub(super) fn extend_selection(
|
|
&mut self,
|
|
position: DisplayPoint,
|
|
click_count: usize,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
|
|
let tail = self
|
|
.selections
|
|
.newest::<MultiBufferOffset>(&display_map)
|
|
.tail();
|
|
let click_count = click_count.max(match self.selections.select_mode() {
|
|
SelectMode::Character => 1,
|
|
SelectMode::Word(_) => 2,
|
|
SelectMode::Line(_) => 3,
|
|
SelectMode::All => 4,
|
|
});
|
|
self.begin_selection(position, false, click_count, window, cx);
|
|
|
|
let tail_anchor = display_map.buffer_snapshot().anchor_before(tail);
|
|
|
|
let current_selection = match self.selections.select_mode() {
|
|
SelectMode::Character | SelectMode::All => tail_anchor..tail_anchor,
|
|
SelectMode::Word(range) | SelectMode::Line(range) => range.clone(),
|
|
};
|
|
|
|
let Some((mut pending_selection, mut pending_mode)) = self.pending_selection_and_mode()
|
|
else {
|
|
log::error!("extend_selection dispatched with no pending selection");
|
|
return;
|
|
};
|
|
|
|
if pending_selection
|
|
.start
|
|
.cmp(¤t_selection.start, display_map.buffer_snapshot())
|
|
== Ordering::Greater
|
|
{
|
|
pending_selection.start = current_selection.start;
|
|
}
|
|
if pending_selection
|
|
.end
|
|
.cmp(¤t_selection.end, display_map.buffer_snapshot())
|
|
== Ordering::Less
|
|
{
|
|
pending_selection.end = current_selection.end;
|
|
pending_selection.reversed = true;
|
|
}
|
|
|
|
match &mut pending_mode {
|
|
SelectMode::Word(range) | SelectMode::Line(range) => *range = current_selection,
|
|
_ => {}
|
|
}
|
|
|
|
let effects = if EditorSettings::get_global(cx).autoscroll_on_clicks {
|
|
SelectionEffects::scroll(Autoscroll::fit())
|
|
} else {
|
|
SelectionEffects::no_scroll()
|
|
};
|
|
|
|
self.change_selections(effects, window, cx, |s| {
|
|
s.set_pending(pending_selection.clone(), pending_mode);
|
|
s.set_is_extending(true);
|
|
});
|
|
}
|
|
|
|
pub(super) fn begin_selection(
|
|
&mut self,
|
|
position: DisplayPoint,
|
|
add: bool,
|
|
click_count: usize,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
if !self.focus_handle.is_focused(window) {
|
|
self.last_focused_descendant = None;
|
|
window.focus(&self.focus_handle, cx);
|
|
}
|
|
|
|
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
|
|
let buffer = display_map.buffer_snapshot();
|
|
let position = display_map.clip_point(position, Bias::Left);
|
|
|
|
let start;
|
|
let end;
|
|
let mode;
|
|
let mut auto_scroll;
|
|
match click_count {
|
|
1 => {
|
|
start = buffer.anchor_before(position.to_point(&display_map));
|
|
end = start;
|
|
mode = SelectMode::Character;
|
|
auto_scroll = true;
|
|
}
|
|
2 => {
|
|
let position = display_map
|
|
.clip_point(position, Bias::Left)
|
|
.to_offset(&display_map, Bias::Left);
|
|
let (range, _) = buffer.surrounding_word(position, None);
|
|
start = buffer.anchor_before(range.start);
|
|
end = buffer.anchor_before(range.end);
|
|
mode = SelectMode::Word(start..end);
|
|
auto_scroll = true;
|
|
}
|
|
3 => {
|
|
let position = display_map
|
|
.clip_point(position, Bias::Left)
|
|
.to_point(&display_map);
|
|
let line_start = display_map.prev_line_boundary(position).0;
|
|
let next_line_start = buffer.clip_point(
|
|
display_map.next_line_boundary(position).0 + Point::new(1, 0),
|
|
Bias::Left,
|
|
);
|
|
start = buffer.anchor_before(line_start);
|
|
end = buffer.anchor_before(next_line_start);
|
|
mode = SelectMode::Line(start..end);
|
|
auto_scroll = true;
|
|
}
|
|
_ => {
|
|
start = buffer.anchor_before(MultiBufferOffset(0));
|
|
end = buffer.anchor_before(buffer.len());
|
|
mode = SelectMode::All;
|
|
auto_scroll = false;
|
|
}
|
|
}
|
|
auto_scroll &= EditorSettings::get_global(cx).autoscroll_on_clicks;
|
|
|
|
let point_to_delete: Option<usize> = {
|
|
let selected_points: Vec<Selection<Point>> =
|
|
self.selections.disjoint_in_range(start..end, &display_map);
|
|
|
|
if !add || click_count > 1 {
|
|
None
|
|
} else if !selected_points.is_empty() {
|
|
Some(selected_points[0].id)
|
|
} else {
|
|
let clicked_point_already_selected =
|
|
self.selections.disjoint_anchors().iter().find(|selection| {
|
|
selection.start.to_point(buffer) == start.to_point(buffer)
|
|
|| selection.end.to_point(buffer) == end.to_point(buffer)
|
|
});
|
|
|
|
clicked_point_already_selected.map(|selection| selection.id)
|
|
}
|
|
};
|
|
|
|
let selections_count = self.selections.count();
|
|
let effects = if auto_scroll {
|
|
SelectionEffects::default()
|
|
} else {
|
|
SelectionEffects::no_scroll()
|
|
};
|
|
|
|
self.change_selections(effects, window, cx, |s| {
|
|
if let Some(point_to_delete) = point_to_delete {
|
|
s.delete(point_to_delete);
|
|
|
|
if selections_count == 1 {
|
|
s.set_pending_anchor_range(start..end, mode);
|
|
}
|
|
} else {
|
|
if !add {
|
|
s.clear_disjoint();
|
|
}
|
|
|
|
s.set_pending_anchor_range(start..end, mode);
|
|
}
|
|
});
|
|
}
|
|
|
|
pub(super) fn update_selection(
|
|
&mut self,
|
|
position: DisplayPoint,
|
|
goal_column: u32,
|
|
scroll_delta: gpui::Point<f32>,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
|
|
|
|
if self.columnar_selection_state.is_some() {
|
|
self.select_columns(position, goal_column, &display_map, window, cx);
|
|
} else if let Some((mut pending, mode)) = self.pending_selection_and_mode() {
|
|
let buffer = display_map.buffer_snapshot();
|
|
let head;
|
|
let tail;
|
|
match &mode {
|
|
SelectMode::Character => {
|
|
head = position.to_point(&display_map);
|
|
tail = pending.tail().to_point(buffer);
|
|
}
|
|
SelectMode::Word(original_range) => {
|
|
let offset = display_map
|
|
.clip_point(position, Bias::Left)
|
|
.to_offset(&display_map, Bias::Left);
|
|
let original_range = original_range.to_offset(buffer);
|
|
|
|
let head_offset = if buffer.is_inside_word(offset, None)
|
|
|| original_range.contains(&offset)
|
|
{
|
|
let (word_range, _) = buffer.surrounding_word(offset, None);
|
|
if word_range.start < original_range.start {
|
|
word_range.start
|
|
} else {
|
|
word_range.end
|
|
}
|
|
} else {
|
|
offset
|
|
};
|
|
|
|
head = head_offset.to_point(buffer);
|
|
if head_offset <= original_range.start {
|
|
tail = original_range.end.to_point(buffer);
|
|
} else {
|
|
tail = original_range.start.to_point(buffer);
|
|
}
|
|
}
|
|
SelectMode::Line(original_range) => {
|
|
let original_range = original_range.to_point(display_map.buffer_snapshot());
|
|
|
|
let position = display_map
|
|
.clip_point(position, Bias::Left)
|
|
.to_point(&display_map);
|
|
let line_start = display_map.prev_line_boundary(position).0;
|
|
let next_line_start = buffer.clip_point(
|
|
display_map.next_line_boundary(position).0 + Point::new(1, 0),
|
|
Bias::Left,
|
|
);
|
|
|
|
if line_start < original_range.start {
|
|
head = line_start
|
|
} else {
|
|
head = next_line_start
|
|
}
|
|
|
|
if head <= original_range.start {
|
|
tail = original_range.end;
|
|
} else {
|
|
tail = original_range.start;
|
|
}
|
|
}
|
|
SelectMode::All => {
|
|
return;
|
|
}
|
|
};
|
|
|
|
if head < tail {
|
|
pending.start = buffer.anchor_before(head);
|
|
pending.end = buffer.anchor_before(tail);
|
|
pending.reversed = true;
|
|
} else {
|
|
pending.start = buffer.anchor_before(tail);
|
|
pending.end = buffer.anchor_before(head);
|
|
pending.reversed = false;
|
|
}
|
|
|
|
self.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
|
s.set_pending(pending.clone(), mode);
|
|
});
|
|
} else {
|
|
log::error!("update_selection dispatched with no pending selection");
|
|
return;
|
|
}
|
|
|
|
self.apply_scroll_delta(scroll_delta, window, cx);
|
|
cx.notify();
|
|
}
|
|
|
|
pub(super) fn end_selection(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
|
self.columnar_selection_state.take();
|
|
if let Some(pending_mode) = self.selections.pending_mode() {
|
|
let selections = self
|
|
.selections
|
|
.all::<MultiBufferOffset>(&self.display_snapshot(cx));
|
|
self.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
|
s.select(selections);
|
|
s.clear_pending();
|
|
if s.is_extending() {
|
|
s.set_is_extending(false);
|
|
} else {
|
|
s.set_select_mode(pending_mode);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
fn begin_columnar_selection(
|
|
&mut self,
|
|
position: DisplayPoint,
|
|
goal_column: u32,
|
|
reset: bool,
|
|
mode: ColumnarMode,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
if !self.focus_handle.is_focused(window) {
|
|
self.last_focused_descendant = None;
|
|
window.focus(&self.focus_handle, cx);
|
|
}
|
|
|
|
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
|
|
|
|
if reset {
|
|
let pointer_position = display_map
|
|
.buffer_snapshot()
|
|
.anchor_before(position.to_point(&display_map));
|
|
|
|
self.change_selections(
|
|
SelectionEffects::scroll(Autoscroll::newest()),
|
|
window,
|
|
cx,
|
|
|s| {
|
|
s.clear_disjoint();
|
|
s.set_pending_anchor_range(
|
|
pointer_position..pointer_position,
|
|
SelectMode::Character,
|
|
);
|
|
},
|
|
);
|
|
};
|
|
|
|
let tail = self.selections.newest::<Point>(&display_map).tail();
|
|
let selection_anchor = display_map.buffer_snapshot().anchor_before(tail);
|
|
self.columnar_selection_state = match mode {
|
|
ColumnarMode::FromMouse => Some(ColumnarSelectionState::FromMouse {
|
|
selection_tail: selection_anchor,
|
|
display_point: if reset {
|
|
if position.column() != goal_column {
|
|
Some(DisplayPoint::new(position.row(), goal_column))
|
|
} else {
|
|
None
|
|
}
|
|
} else {
|
|
None
|
|
},
|
|
}),
|
|
ColumnarMode::FromSelection => Some(ColumnarSelectionState::FromSelection {
|
|
selection_tail: selection_anchor,
|
|
}),
|
|
};
|
|
|
|
if !reset {
|
|
self.select_columns(position, goal_column, &display_map, window, cx);
|
|
}
|
|
}
|
|
|
|
fn selections_did_change(
|
|
&mut self,
|
|
local: bool,
|
|
old_cursor_position: &Anchor,
|
|
effects: SelectionEffects,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
self.last_selection_from_search = effects.from_search;
|
|
window.invalidate_character_coordinates();
|
|
|
|
// Copy selections to primary selection buffer
|
|
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
|
if local {
|
|
let selections = self
|
|
.selections
|
|
.all::<MultiBufferOffset>(&self.display_snapshot(cx));
|
|
let buffer_handle = self.buffer.read(cx).read(cx);
|
|
|
|
let mut text = String::new();
|
|
for (index, selection) in selections.iter().enumerate() {
|
|
let text_for_selection = buffer_handle
|
|
.text_for_range(selection.start..selection.end)
|
|
.collect::<String>();
|
|
|
|
text.push_str(&text_for_selection);
|
|
if index != selections.len() - 1 {
|
|
text.push('\n');
|
|
}
|
|
}
|
|
|
|
if !text.is_empty() {
|
|
cx.write_to_primary(ClipboardItem::new_string(text));
|
|
}
|
|
}
|
|
|
|
let selection_anchors = self.selections.disjoint_anchors_arc();
|
|
|
|
if self.focus_handle.is_focused(window) && self.leader_id.is_none() {
|
|
self.buffer.update(cx, |buffer, cx| {
|
|
buffer.set_active_selections(
|
|
&selection_anchors,
|
|
self.selections.line_mode(),
|
|
self.cursor_shape,
|
|
cx,
|
|
)
|
|
});
|
|
}
|
|
let display_map = self
|
|
.display_map
|
|
.update(cx, |display_map, cx| display_map.snapshot(cx));
|
|
let buffer = display_map.buffer_snapshot();
|
|
if self.selections.count() == 1 {
|
|
self.add_selections_state = None;
|
|
}
|
|
self.select_next_state = None;
|
|
self.select_prev_state = None;
|
|
self.select_syntax_node_history.try_clear();
|
|
self.invalidate_autoclose_regions(&selection_anchors, buffer);
|
|
self.snippet_stack.invalidate(&selection_anchors, buffer);
|
|
self.take_rename(false, window, cx);
|
|
|
|
let newest_selection = self.selections.newest_anchor();
|
|
let new_cursor_position = newest_selection.head();
|
|
let selection_start = newest_selection.start;
|
|
|
|
if effects.nav_history.is_none() || effects.nav_history == Some(true) {
|
|
self.push_to_nav_history(
|
|
*old_cursor_position,
|
|
Some(new_cursor_position.to_point(buffer)),
|
|
false,
|
|
effects.nav_history == Some(true),
|
|
cx,
|
|
);
|
|
}
|
|
|
|
if local {
|
|
if let Some((anchor, _)) = buffer.anchor_to_buffer_anchor(new_cursor_position) {
|
|
self.register_buffer(anchor.buffer_id, cx);
|
|
}
|
|
|
|
let mut context_menu = self.context_menu.borrow_mut();
|
|
let completion_menu = match context_menu.as_ref() {
|
|
Some(CodeContextMenu::Completions(menu)) => Some(menu),
|
|
Some(CodeContextMenu::CodeActions(_)) => {
|
|
*context_menu = None;
|
|
None
|
|
}
|
|
None => None,
|
|
};
|
|
let completion_position = completion_menu.map(|menu| menu.initial_position);
|
|
drop(context_menu);
|
|
|
|
if effects.completions
|
|
&& let Some(completion_position) = completion_position
|
|
{
|
|
let start_offset = selection_start.to_offset(buffer);
|
|
let position_matches = start_offset == completion_position.to_offset(buffer);
|
|
let continue_showing = if let Some((snap, ..)) =
|
|
buffer.point_to_buffer_offset(completion_position)
|
|
&& !snap.capability.editable()
|
|
{
|
|
false
|
|
} else if position_matches {
|
|
if self.snippet_stack.is_empty() {
|
|
buffer.char_kind_before(start_offset, Some(CharScopeContext::Completion))
|
|
== Some(CharKind::Word)
|
|
} else {
|
|
// Snippet choices can be shown even when the cursor is in whitespace.
|
|
// Dismissing the menu with actions like backspace is handled by
|
|
// invalidation regions.
|
|
true
|
|
}
|
|
} else {
|
|
false
|
|
};
|
|
|
|
if continue_showing {
|
|
self.open_or_update_completions_menu(None, None, false, window, cx);
|
|
} else {
|
|
self.hide_context_menu(window, cx);
|
|
}
|
|
}
|
|
|
|
hide_hover(self, cx);
|
|
|
|
self.refresh_code_actions_for_selection(window, cx);
|
|
self.refresh_document_highlights(cx);
|
|
refresh_linked_ranges(self, window, cx);
|
|
|
|
self.refresh_selected_text_highlights(&display_map, false, window, cx);
|
|
self.refresh_matching_bracket_highlights(&display_map, cx);
|
|
self.refresh_outline_symbols_at_cursor(cx);
|
|
self.update_visible_edit_prediction(window, cx);
|
|
self.hide_blame_popover(true, cx);
|
|
if self.git_blame_inline_enabled {
|
|
self.start_inline_blame_timer(window, cx);
|
|
}
|
|
}
|
|
|
|
self.blink_manager.update(cx, BlinkManager::pause_blinking);
|
|
|
|
if local && !self.suppress_selection_callback {
|
|
if let Some(callback) = self.on_local_selections_changed.as_ref() {
|
|
let cursor_position = self.selections.newest::<Point>(&display_map).head();
|
|
callback(cursor_position, window, cx);
|
|
}
|
|
}
|
|
|
|
cx.emit(EditorEvent::SelectionsChanged { local });
|
|
|
|
let selections = &self.selections.disjoint_anchors_arc();
|
|
if local && let Some(buffer_snapshot) = buffer.as_singleton() {
|
|
let inmemory_selections = selections
|
|
.iter()
|
|
.map(|s| {
|
|
let start = s.range().start.text_anchor_in(buffer_snapshot);
|
|
let end = s.range().end.text_anchor_in(buffer_snapshot);
|
|
(start..end).to_point(buffer_snapshot)
|
|
})
|
|
.collect();
|
|
self.update_restoration_data(cx, |data| {
|
|
data.selections = inmemory_selections;
|
|
});
|
|
|
|
if WorkspaceSettings::get(None, cx).restore_on_startup
|
|
!= RestoreOnStartupBehavior::EmptyTab
|
|
&& let Some(workspace_id) = self.workspace_serialization_id(cx)
|
|
{
|
|
let snapshot = self.buffer().read(cx).snapshot(cx);
|
|
let selections = selections.clone();
|
|
let background_executor = cx.background_executor().clone();
|
|
let editor_id = cx.entity().entity_id().as_u64() as ItemId;
|
|
let db = EditorDb::global(cx);
|
|
self.serialize_selections = cx.background_spawn(async move {
|
|
background_executor.timer(SERIALIZATION_THROTTLE_TIME).await;
|
|
let db_selections = selections
|
|
.iter()
|
|
.map(|selection| {
|
|
(
|
|
selection.start.to_offset(&snapshot).0,
|
|
selection.end.to_offset(&snapshot).0,
|
|
)
|
|
})
|
|
.collect();
|
|
|
|
db.save_editor_selections(editor_id, workspace_id, db_selections)
|
|
.await
|
|
.with_context(|| {
|
|
format!(
|
|
"persisting editor selections for editor {editor_id}, \
|
|
workspace {workspace_id:?}"
|
|
)
|
|
})
|
|
.log_err();
|
|
});
|
|
}
|
|
}
|
|
|
|
cx.notify();
|
|
}
|
|
|
|
fn apply_selection_effects(
|
|
&mut self,
|
|
state: DeferredSelectionEffectsState,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
if state.changed {
|
|
self.selection_history.push(state.history_entry);
|
|
|
|
if let Some(autoscroll) = state.effects.scroll {
|
|
self.request_autoscroll(autoscroll, cx);
|
|
}
|
|
|
|
let old_cursor_position = &state.old_cursor_position;
|
|
|
|
self.selections_did_change(true, old_cursor_position, state.effects, window, cx);
|
|
|
|
if self.should_open_signature_help_automatically(old_cursor_position, cx) {
|
|
self.show_signature_help_auto(window, cx);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn select_columns(
|
|
&mut self,
|
|
head: DisplayPoint,
|
|
goal_column: u32,
|
|
display_map: &DisplaySnapshot,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
let Some(columnar_state) = self.columnar_selection_state.as_ref() else {
|
|
return;
|
|
};
|
|
|
|
let tail = match columnar_state {
|
|
ColumnarSelectionState::FromMouse {
|
|
selection_tail,
|
|
display_point,
|
|
} => display_point.unwrap_or_else(|| selection_tail.to_display_point(display_map)),
|
|
ColumnarSelectionState::FromSelection { selection_tail } => {
|
|
selection_tail.to_display_point(display_map)
|
|
}
|
|
};
|
|
|
|
let start_row = cmp::min(tail.row(), head.row());
|
|
let end_row = cmp::max(tail.row(), head.row());
|
|
|
|
// Anchor the columnar rectangle in x pixels rather than byte columns so
|
|
// rows with multi-byte characters (e.g. diacritics) stay visually aligned.
|
|
let text_layout_details = self.text_layout_details(window, cx);
|
|
|
|
// The mouse handlers encode drags past a line's end as extra columns
|
|
// beyond the line length, in em layout widths (see
|
|
// `PositionMap::point_for_position`). `x_for_display_point` clamps at
|
|
// the line's width, so convert that overshoot back to pixels with the
|
|
// same unit to keep the rectangle tracking the mouse past short lines.
|
|
let font_id = text_layout_details
|
|
.text_system
|
|
.resolve_font(&text_layout_details.editor_style.text.font());
|
|
let font_size = text_layout_details
|
|
.editor_style
|
|
.text
|
|
.font_size
|
|
.to_pixels(text_layout_details.rem_size);
|
|
let em_layout_width = text_layout_details
|
|
.text_system
|
|
.em_layout_width(font_id, font_size);
|
|
let x_for_unclipped_point = |point: DisplayPoint| {
|
|
let line_len = display_map.line_len(point.row());
|
|
if point.column() > line_len {
|
|
let eol_x = display_map.x_for_display_point(
|
|
DisplayPoint::new(point.row(), line_len),
|
|
&text_layout_details,
|
|
);
|
|
eol_x + em_layout_width * (point.column() - line_len) as f32
|
|
} else {
|
|
display_map.x_for_display_point(point, &text_layout_details)
|
|
}
|
|
};
|
|
|
|
let tail_x = x_for_unclipped_point(tail);
|
|
let head_x = x_for_unclipped_point(DisplayPoint::new(head.row(), goal_column));
|
|
let start_x = tail_x.min(head_x);
|
|
let end_x = tail_x.max(head_x);
|
|
let reversed = head_x < tail_x;
|
|
|
|
let selection_ranges = (start_row.0..=end_row.0)
|
|
.map(DisplayRow)
|
|
.filter_map(|row| {
|
|
if display_map.is_block_line(row) {
|
|
return None;
|
|
}
|
|
|
|
let layout = display_map.layout_row(row, &text_layout_details);
|
|
if matches!(columnar_state, ColumnarSelectionState::FromSelection { .. })
|
|
&& start_x > layout.width
|
|
{
|
|
return None;
|
|
}
|
|
|
|
let start_column = layout.closest_index_for_x(start_x) as u32;
|
|
let end_column = layout.closest_index_for_x(end_x) as u32;
|
|
|
|
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)
|
|
}
|
|
})
|
|
.collect::<Vec<_>>();
|
|
if selection_ranges.is_empty() {
|
|
return;
|
|
}
|
|
|
|
let ranges = match columnar_state {
|
|
ColumnarSelectionState::FromMouse { .. } => {
|
|
let mut non_empty_ranges = selection_ranges
|
|
.iter()
|
|
.filter(|selection_range| selection_range.start != selection_range.end)
|
|
.peekable();
|
|
if non_empty_ranges.peek().is_some() {
|
|
non_empty_ranges.cloned().collect()
|
|
} else {
|
|
selection_ranges
|
|
}
|
|
}
|
|
_ => selection_ranges,
|
|
};
|
|
|
|
self.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
|
s.select_ranges(ranges);
|
|
});
|
|
cx.notify();
|
|
}
|
|
|
|
fn pending_selection_and_mode(&self) -> Option<(Selection<Anchor>, SelectMode)> {
|
|
Some((
|
|
self.selections.pending_anchor()?.clone(),
|
|
self.selections.pending_mode()?,
|
|
))
|
|
}
|
|
|
|
fn add_selection(
|
|
&mut self,
|
|
above: bool,
|
|
skip_soft_wrap: bool,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
|
|
let all_selections = self.selections.all::<Point>(&display_map);
|
|
let text_layout_details = self.text_layout_details(window, cx);
|
|
|
|
let (mut columnar_selections, new_selections_to_columnarize) = {
|
|
if let Some(state) = self.add_selections_state.as_ref() {
|
|
let columnar_selection_ids: HashSet<_> = state
|
|
.groups
|
|
.iter()
|
|
.flat_map(|group| group.stack.iter())
|
|
.copied()
|
|
.collect();
|
|
|
|
all_selections
|
|
.into_iter()
|
|
.partition(|s| columnar_selection_ids.contains(&s.id))
|
|
} else {
|
|
(Vec::new(), all_selections)
|
|
}
|
|
};
|
|
|
|
let mut state = self
|
|
.add_selections_state
|
|
.take()
|
|
.unwrap_or_else(|| AddSelectionsState { groups: Vec::new() });
|
|
|
|
for selection in new_selections_to_columnarize {
|
|
let range = selection.display_range(&display_map).sorted();
|
|
let start_x = display_map.x_for_display_point(range.start, &text_layout_details);
|
|
let end_x = display_map.x_for_display_point(range.end, &text_layout_details);
|
|
let positions = start_x.min(end_x)..start_x.max(end_x);
|
|
let mut stack = Vec::new();
|
|
for row in range.start.row().0..=range.end.row().0 {
|
|
if let Some(selection) = self.selections.build_columnar_selection(
|
|
&display_map,
|
|
DisplayRow(row),
|
|
&positions,
|
|
selection.reversed,
|
|
&text_layout_details,
|
|
) {
|
|
stack.push(selection.id);
|
|
columnar_selections.push(selection);
|
|
}
|
|
}
|
|
if !stack.is_empty() {
|
|
if above {
|
|
stack.reverse();
|
|
}
|
|
state.groups.push(AddSelectionsGroup { above, stack });
|
|
}
|
|
}
|
|
|
|
let mut final_selections = Vec::new();
|
|
let end_row = if above {
|
|
DisplayRow(0)
|
|
} else {
|
|
display_map.max_point().row()
|
|
};
|
|
|
|
// When `skip_soft_wrap` is true, we use UTF-16 columns instead of pixel
|
|
// positions to place new selections, so we need to keep track of the
|
|
// column range of the oldest selection in each group, because
|
|
// intermediate selections may have been clamped to shorter lines.
|
|
let mut goal_columns_by_selection_id = if skip_soft_wrap {
|
|
let mut map = HashMap::default();
|
|
for group in state.groups.iter() {
|
|
if let Some(oldest_id) = group.stack.first() {
|
|
if let Some(oldest_selection) =
|
|
columnar_selections.iter().find(|s| s.id == *oldest_id)
|
|
{
|
|
let snapshot = display_map.buffer_snapshot();
|
|
let start_col =
|
|
snapshot.point_to_point_utf16(oldest_selection.start).column;
|
|
let end_col = snapshot.point_to_point_utf16(oldest_selection.end).column;
|
|
let goal_columns = start_col.min(end_col)..start_col.max(end_col);
|
|
for id in &group.stack {
|
|
map.insert(*id, goal_columns.clone());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
map
|
|
} else {
|
|
HashMap::default()
|
|
};
|
|
|
|
let mut last_added_item_per_group = HashMap::default();
|
|
for group in state.groups.iter_mut() {
|
|
if let Some(last_id) = group.stack.last() {
|
|
last_added_item_per_group.insert(*last_id, group);
|
|
}
|
|
}
|
|
|
|
for selection in columnar_selections {
|
|
if let Some(group) = last_added_item_per_group.get_mut(&selection.id) {
|
|
if above == group.above {
|
|
let range = selection.display_range(&display_map).sorted();
|
|
debug_assert_eq!(range.start.row(), range.end.row());
|
|
let row = range.start.row();
|
|
let positions =
|
|
if let SelectionGoal::HorizontalRange { start, end } = selection.goal {
|
|
Pixels::from(start)..Pixels::from(end)
|
|
} else {
|
|
let start_x =
|
|
display_map.x_for_display_point(range.start, &text_layout_details);
|
|
let end_x =
|
|
display_map.x_for_display_point(range.end, &text_layout_details);
|
|
start_x.min(end_x)..start_x.max(end_x)
|
|
};
|
|
|
|
let maybe_new_selection = if skip_soft_wrap {
|
|
let goal_columns = goal_columns_by_selection_id
|
|
.remove(&selection.id)
|
|
.unwrap_or_else(|| {
|
|
let snapshot = display_map.buffer_snapshot();
|
|
let start_col =
|
|
snapshot.point_to_point_utf16(selection.start).column;
|
|
let end_col = snapshot.point_to_point_utf16(selection.end).column;
|
|
start_col.min(end_col)..start_col.max(end_col)
|
|
});
|
|
self.selections.find_next_columnar_selection_by_buffer_row(
|
|
&display_map,
|
|
row,
|
|
end_row,
|
|
above,
|
|
&goal_columns,
|
|
selection.reversed,
|
|
&text_layout_details,
|
|
)
|
|
} else {
|
|
self.selections.find_next_columnar_selection_by_display_row(
|
|
&display_map,
|
|
row,
|
|
end_row,
|
|
above,
|
|
&positions,
|
|
selection.reversed,
|
|
&text_layout_details,
|
|
)
|
|
};
|
|
|
|
if let Some(new_selection) = maybe_new_selection {
|
|
group.stack.push(new_selection.id);
|
|
if above {
|
|
final_selections.push(new_selection);
|
|
final_selections.push(selection);
|
|
} else {
|
|
final_selections.push(selection);
|
|
final_selections.push(new_selection);
|
|
}
|
|
} else {
|
|
final_selections.push(selection);
|
|
}
|
|
} else {
|
|
group.stack.pop();
|
|
}
|
|
} else {
|
|
final_selections.push(selection);
|
|
}
|
|
}
|
|
|
|
self.change_selections(Default::default(), window, cx, |s| {
|
|
s.select(final_selections);
|
|
});
|
|
|
|
let final_selection_ids: HashSet<_> = self
|
|
.selections
|
|
.all::<Point>(&display_map)
|
|
.iter()
|
|
.map(|s| s.id)
|
|
.collect();
|
|
state.groups.retain_mut(|group| {
|
|
// selections might get merged above so we remove invalid items from stacks
|
|
group.stack.retain(|id| final_selection_ids.contains(id));
|
|
|
|
// single selection in stack can be treated as initial state
|
|
group.stack.len() > 1
|
|
});
|
|
|
|
if !state.groups.is_empty() {
|
|
self.add_selections_state = Some(state);
|
|
}
|
|
}
|
|
|
|
fn select_match_ranges(
|
|
&mut self,
|
|
range: Range<MultiBufferOffset>,
|
|
reversed: bool,
|
|
replace_newest: bool,
|
|
auto_scroll: Option<Autoscroll>,
|
|
window: &mut Window,
|
|
cx: &mut Context<Editor>,
|
|
) {
|
|
self.unfold_ranges(
|
|
std::slice::from_ref(&range),
|
|
false,
|
|
auto_scroll.is_some(),
|
|
cx,
|
|
);
|
|
let effects = if let Some(scroll) = auto_scroll {
|
|
SelectionEffects::scroll(scroll)
|
|
} else {
|
|
SelectionEffects::no_scroll()
|
|
};
|
|
self.change_selections(effects, window, cx, |s| {
|
|
if replace_newest {
|
|
s.delete(s.newest_anchor().id);
|
|
}
|
|
if reversed {
|
|
s.insert_range(range.end..range.start);
|
|
} else {
|
|
s.insert_range(range);
|
|
}
|
|
});
|
|
}
|
|
|
|
fn select_next_match_internal(
|
|
&mut self,
|
|
display_map: &DisplaySnapshot,
|
|
replace_newest: bool,
|
|
autoscroll: Option<Autoscroll>,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) -> Result<()> {
|
|
let buffer = display_map.buffer_snapshot();
|
|
let mut selections = self.selections.all::<MultiBufferOffset>(&display_map);
|
|
if let Some(mut select_next_state) = self.select_next_state.take() {
|
|
let query = &select_next_state.query;
|
|
if !select_next_state.done {
|
|
let first_selection = selections
|
|
.iter()
|
|
.min_by_key(|s| s.id)
|
|
.context("missing selection for select next action")?;
|
|
let last_selection = selections
|
|
.iter()
|
|
.max_by_key(|s| s.id)
|
|
.context("missing selection for select next action")?;
|
|
let mut next_selected_range = None;
|
|
|
|
let bytes_after_last_selection =
|
|
buffer.bytes_in_range(last_selection.end..buffer.len());
|
|
let bytes_before_first_selection =
|
|
buffer.bytes_in_range(MultiBufferOffset(0)..first_selection.start);
|
|
let query_matches = query
|
|
.stream_find_iter(bytes_after_last_selection)
|
|
.map(|result| (last_selection.end, result))
|
|
.chain(
|
|
query
|
|
.stream_find_iter(bytes_before_first_selection)
|
|
.map(|result| (MultiBufferOffset(0), result)),
|
|
);
|
|
|
|
for (start_offset, query_match) in query_matches {
|
|
let query_match = query_match.context("query match for select next action")?;
|
|
let offset_range =
|
|
start_offset + query_match.start()..start_offset + query_match.end();
|
|
|
|
if !select_next_state.wordwise
|
|
|| (!buffer.is_inside_word(offset_range.start, None)
|
|
&& !buffer.is_inside_word(offset_range.end, None))
|
|
{
|
|
let idx = selections
|
|
.partition_point(|selection| selection.end <= offset_range.start);
|
|
let overlaps = selections
|
|
.get(idx)
|
|
.map_or(false, |selection| selection.start < offset_range.end);
|
|
|
|
if !overlaps {
|
|
next_selected_range = Some(offset_range);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if let Some(next_selected_range) = next_selected_range {
|
|
self.select_match_ranges(
|
|
next_selected_range,
|
|
last_selection.reversed,
|
|
replace_newest,
|
|
autoscroll,
|
|
window,
|
|
cx,
|
|
);
|
|
} else {
|
|
select_next_state.done = true;
|
|
}
|
|
}
|
|
|
|
self.select_next_state = Some(select_next_state);
|
|
} else {
|
|
let mut only_carets = true;
|
|
let mut same_text_selected = true;
|
|
let mut selected_text = None;
|
|
|
|
let mut selections_iter = selections.iter().peekable();
|
|
while let Some(selection) = selections_iter.next() {
|
|
if selection.start != selection.end {
|
|
only_carets = false;
|
|
}
|
|
|
|
if same_text_selected {
|
|
if selected_text.is_none() {
|
|
selected_text =
|
|
Some(buffer.text_for_range(selection.range()).collect::<String>());
|
|
}
|
|
|
|
if let Some(next_selection) = selections_iter.peek() {
|
|
if next_selection.len() == selection.len() {
|
|
let next_selected_text = buffer
|
|
.text_for_range(next_selection.range())
|
|
.collect::<String>();
|
|
if Some(next_selected_text) != selected_text {
|
|
same_text_selected = false;
|
|
selected_text = None;
|
|
}
|
|
} else {
|
|
same_text_selected = false;
|
|
selected_text = None;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if only_carets {
|
|
for selection in &mut selections {
|
|
let (word_range, _) = buffer.surrounding_word(selection.start, None);
|
|
selection.start = word_range.start;
|
|
selection.end = word_range.end;
|
|
selection.goal = SelectionGoal::None;
|
|
selection.reversed = false;
|
|
self.select_match_ranges(
|
|
selection.start..selection.end,
|
|
selection.reversed,
|
|
replace_newest,
|
|
autoscroll,
|
|
window,
|
|
cx,
|
|
);
|
|
}
|
|
|
|
if selections.len() == 1 {
|
|
let selection = selections
|
|
.last()
|
|
.expect("ensured that there's only one selection");
|
|
let query = buffer
|
|
.text_for_range(selection.start..selection.end)
|
|
.collect::<String>();
|
|
let is_empty = query.is_empty();
|
|
let select_state = SelectNextState {
|
|
query: self.build_query(&[query], cx)?,
|
|
wordwise: true,
|
|
done: is_empty,
|
|
};
|
|
self.select_next_state = Some(select_state);
|
|
} else {
|
|
self.select_next_state = None;
|
|
}
|
|
} else if let Some(selected_text) = selected_text {
|
|
self.select_next_state = Some(SelectNextState {
|
|
query: self.build_query(&[selected_text], cx)?,
|
|
wordwise: false,
|
|
done: false,
|
|
});
|
|
self.select_next_match_internal(
|
|
display_map,
|
|
replace_newest,
|
|
autoscroll,
|
|
window,
|
|
cx,
|
|
)?;
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn build_query<I, P>(&self, patterns: I, cx: &Context<Self>) -> Result<AhoCorasick, BuildError>
|
|
where
|
|
I: IntoIterator<Item = P>,
|
|
P: AsRef<[u8]>,
|
|
{
|
|
let case_sensitive = self
|
|
.select_next_is_case_sensitive
|
|
.unwrap_or_else(|| EditorSettings::get_global(cx).search.case_sensitive);
|
|
|
|
let mut builder = AhoCorasickBuilder::new();
|
|
builder.ascii_case_insensitive(!case_sensitive);
|
|
builder.build(patterns)
|
|
}
|
|
|
|
fn find_syntax_node_boundary(
|
|
&self,
|
|
selection_pos: MultiBufferOffset,
|
|
move_to_end: bool,
|
|
display_map: &DisplaySnapshot,
|
|
buffer: &MultiBufferSnapshot,
|
|
) -> MultiBufferOffset {
|
|
let old_range = selection_pos..selection_pos;
|
|
let mut new_pos = selection_pos;
|
|
let mut search_range = old_range;
|
|
while let Some((node, range)) = buffer.syntax_ancestor(search_range.clone()) {
|
|
search_range = range.clone();
|
|
if !node.is_named()
|
|
|| display_map.intersects_fold(range.start)
|
|
|| display_map.intersects_fold(range.end)
|
|
// If cursor is already at the end of the syntax node, continue searching
|
|
|| (move_to_end && range.end == selection_pos)
|
|
// If cursor is already at the start of the syntax node, continue searching
|
|
|| (!move_to_end && range.start == selection_pos)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// If we found a string_content node, find the largest parent that is still string_content
|
|
// Enables us to skip to the end of strings without taking multiple steps inside the string
|
|
let (_, final_range) = if node.kind() == "string_content" {
|
|
let mut current_node = node;
|
|
let mut current_range = range;
|
|
while let Some((parent, parent_range)) =
|
|
buffer.syntax_ancestor(current_range.clone())
|
|
{
|
|
if parent.kind() == "string_content" {
|
|
current_node = parent;
|
|
current_range = parent_range;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
(current_node, current_range)
|
|
} else {
|
|
(node, range)
|
|
};
|
|
|
|
new_pos = if move_to_end {
|
|
final_range.end
|
|
} else {
|
|
final_range.start
|
|
};
|
|
|
|
break;
|
|
}
|
|
|
|
new_pos
|
|
}
|
|
|
|
fn move_cursors_to_syntax_nodes(
|
|
&mut self,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
move_to_end: bool,
|
|
) {
|
|
let old_selections: Box<[_]> = self
|
|
.selections
|
|
.all::<MultiBufferOffset>(&self.display_snapshot(cx))
|
|
.into();
|
|
if old_selections.is_empty() {
|
|
return;
|
|
}
|
|
|
|
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
|
|
let buffer = self.buffer.read(cx).snapshot(cx);
|
|
|
|
let new_selections = old_selections
|
|
.iter()
|
|
.map(|selection| {
|
|
if !selection.is_empty() {
|
|
return selection.clone();
|
|
}
|
|
|
|
let selection_pos = selection.head();
|
|
let new_pos = self.find_syntax_node_boundary(
|
|
selection_pos,
|
|
move_to_end,
|
|
&display_map,
|
|
&buffer,
|
|
);
|
|
|
|
Selection {
|
|
id: selection.id,
|
|
start: new_pos,
|
|
end: new_pos,
|
|
goal: SelectionGoal::None,
|
|
reversed: false,
|
|
}
|
|
})
|
|
.collect::<Vec<_>>();
|
|
|
|
self.change_selections(Default::default(), window, cx, |s| {
|
|
s.select(new_selections);
|
|
});
|
|
self.request_autoscroll(Autoscroll::newest(), cx);
|
|
}
|
|
|
|
fn select_to_syntax_nodes(
|
|
&mut self,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
move_to_end: bool,
|
|
) {
|
|
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
|
|
let buffer = self.buffer.read(cx).snapshot(cx);
|
|
let old_selections = self.selections.all::<MultiBufferOffset>(&display_map);
|
|
|
|
let new_selections = old_selections
|
|
.iter()
|
|
.map(|selection| {
|
|
let new_pos = self.find_syntax_node_boundary(
|
|
selection.head(),
|
|
move_to_end,
|
|
&display_map,
|
|
&buffer,
|
|
);
|
|
|
|
let mut new_selection = selection.clone();
|
|
new_selection.set_head(new_pos, SelectionGoal::None);
|
|
new_selection
|
|
})
|
|
.collect::<Vec<_>>();
|
|
|
|
self.change_selections(Default::default(), window, cx, |s| {
|
|
s.select(new_selections);
|
|
});
|
|
}
|
|
}
|