use std::ops::Range;
use crate::{ClipboardItem, Context, Window};
use super::super::InputState;
use super::super::rope_ext::RopeExt as _;
impl InputState {
pub fn has_multiple_cursors(&self) -> bool {
!self.extra_selections.is_empty()
}
pub fn extra_selections(&self) -> &[super::Selection] {
&self.extra_selections
}
pub fn clear_extra_cursors(&mut self, cx: &mut Context<Self>) {
if self.extra_selections.is_empty() {
return;
}
self.extra_selections.clear();
cx.notify();
}
pub(crate) fn add_cursor_above(
&mut self,
_: &super::AddCursorAbove,
_: &mut Window,
cx: &mut Context<Self>,
) {
if !self.multicursor_allowed(cx) {
return;
}
self.add_cursors(-1);
cx.notify();
}
pub(crate) fn add_cursor_below(
&mut self,
_: &super::AddCursorBelow,
_: &mut Window,
cx: &mut Context<Self>,
) {
if !self.multicursor_allowed(cx) {
return;
}
self.add_cursors(1);
cx.notify();
}
fn multicursor_allowed(&self, cx: &mut Context<Self>) -> bool {
if !self.mode.is_multi_line() || self.disabled || self.read_only || self.masked {
cx.propagate();
return false;
}
true
}
fn add_cursors(&mut self, delta_rows: isize) {
let last_row = self.core.text.lines_len().saturating_sub(1);
let mut offsets: Vec<usize> = std::iter::once(self.core.selected_range.end)
.chain(self.extra_selections.iter().map(|sel| sel.end))
.collect();
for offset in offsets.drain(..) {
let point = self.core.text.offset_to_point(offset);
let column = offset.saturating_sub(self.core.text.line_start_offset(point.row));
let target = point.row.saturating_add_signed(delta_rows).min(last_row);
if target == point.row {
continue;
}
let line_start = self.core.text.line_start_offset(target);
let line_len = self.core.text.line_end_offset(target) - line_start;
let at = self
.core
.text
.floor_char_boundary(line_start + column.min(line_len));
self.extra_selections.push(super::Selection::new(at, at));
}
self.normalize_extras();
}
pub(crate) fn normalize_extras(&mut self) {
let primary: Range<usize> = self.core.selected_range.into();
self.extra_selections.sort_by_key(|sel| sel.start);
let mut merged: Vec<super::Selection> = Vec::new();
for sel in self.extra_selections.drain(..) {
if sel.start <= primary.end && primary.start <= sel.end {
continue;
}
if let Some(last) = merged.last_mut() {
if sel.start <= last.end {
last.end = last.end.max(sel.end);
continue;
}
}
merged.push(sel);
}
self.extra_selections = merged;
}
fn cursors_back_to_front(&self) -> Vec<Range<usize>> {
let mut all: Vec<Range<usize>> = std::iter::once(self.core.selected_range.into())
.chain(self.extra_selections.iter().map(|sel| (*sel).into()))
.collect();
all.sort_by_key(|range| std::cmp::Reverse(range.start));
all
}
fn apply_edits_and_track(
&mut self,
edits: &mut Vec<(Range<usize>, String)>,
points: &mut Vec<usize>,
window: &mut Window,
cx: &mut Context<Self>,
) {
edits.sort_by_key(|(range, _)| std::cmp::Reverse(range.start));
let before = self.core.history.undos().len();
for (range, text) in edits.iter() {
let old_len = range.len();
let new_len = text.len();
self.replace_text_in_range_silent(Some(self.range_to_utf16(range)), text, window, cx);
for point in points.iter_mut() {
if *point >= range.end {
*point = point.saturating_add(new_len).saturating_sub(old_len);
} else if *point > range.start {
*point = range.start + new_len;
} else if *point == range.start && new_len > 0 {
*point = range.start + new_len;
}
}
}
self.regroup_history(before);
}
fn collapse_cursors(&mut self, mut points: Vec<usize>, cx: &mut Context<Self>) {
let mut points_iter = points.drain(..);
let primary = points_iter.next().unwrap_or(0);
self.core.selected_range = super::Selection::new(primary, primary);
self.extra_selections = points_iter
.map(|at| super::Selection::new(at, at))
.collect();
self.core.selection_reversed = false;
cx.notify();
}
pub(crate) fn multi_insert(&mut self, text: &str, window: &mut Window, cx: &mut Context<Self>) {
if text.is_empty() {
return;
}
let mut edits: Vec<(Range<usize>, String)> = self
.cursors_back_to_front()
.into_iter()
.map(|range| (range, text.to_string()))
.collect();
let mut points: Vec<usize> = std::iter::once(self.core.selected_range.start)
.chain(self.extra_selections.iter().map(|sel| sel.start))
.collect();
self.apply_edits_and_track(&mut edits, &mut points, window, cx);
self.collapse_cursors(points, cx);
}
pub(crate) fn multi_delete(
&mut self,
backward: bool,
window: &mut Window,
cx: &mut Context<Self>,
) {
let cursors = self.cursors_back_to_front();
let mut ranges = Vec::new();
for range in &cursors {
let delete = if !range.is_empty() {
Some(range.clone())
} else if backward {
let start = self.previous_boundary(range.start);
(start < range.start).then(|| start..range.start)
} else {
let end = self.next_boundary(range.end);
(end > range.end).then(|| range.end..end)
};
if let Some(delete) = delete {
ranges.push(delete);
}
}
if ranges.is_empty() {
return;
}
let mut edits: Vec<(Range<usize>, String)> = ranges
.into_iter()
.map(|range| (range, String::new()))
.collect();
let mut points: Vec<usize> = std::iter::once(self.core.selected_range.start)
.chain(self.extra_selections.iter().map(|sel| sel.start))
.collect();
self.apply_edits_and_track(&mut edits, &mut points, window, cx);
self.collapse_cursors(points, cx);
}
pub(crate) fn copy_cursors(&mut self, cx: &mut Context<Self>) {
let mut selections = self.cursors_back_to_front();
selections.sort_by_key(|range| range.start);
let all_collapsed = selections.iter().all(|range| range.is_empty());
let parts: Vec<String> = if all_collapsed {
selections
.iter()
.map(|range| {
let row = self.core.text.offset_to_point(range.start).row;
self.core
.text
.slice(
self.core.text.line_start_offset(row)
..self.core.text.line_end_offset(row),
)
.to_string()
})
.collect()
} else {
selections
.iter()
.map(|range| self.core.text.slice(range.clone()).to_string())
.collect()
};
cx.write_to_clipboard(ClipboardItem::new_string(parts.join("\n")));
}
pub(crate) fn cut_cursors(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.copy_cursors(cx);
let mut selections = self.cursors_back_to_front();
selections.sort_by_key(|range| range.start);
let all_collapsed = selections.iter().all(|range| range.is_empty());
let mut ranges: Vec<Range<usize>> = if all_collapsed {
selections
.iter()
.map(|range| {
let row = self.core.text.offset_to_point(range.start).row;
self.block_range(row, row)
})
.collect()
} else {
selections
};
ranges.sort_by_key(|range| std::cmp::Reverse(range.start));
let mut edits: Vec<(Range<usize>, String)> = ranges
.into_iter()
.map(|range| (range, String::new()))
.collect();
let mut points: Vec<usize> = std::iter::once(self.core.selected_range.start)
.chain(self.extra_selections.iter().map(|sel| sel.start))
.collect();
self.apply_edits_and_track(&mut edits, &mut points, window, cx);
let len = self.core.text.len();
let points: Vec<usize> = points.into_iter().map(|at| at.min(len)).collect();
self.collapse_cursors(points, cx);
}
pub(crate) fn enter_cursors(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let mut edits = Vec::new();
for range in self.cursors_back_to_front() {
let start = range.start.min(self.core.text.len());
let line_start = self
.core
.text
.line_start_offset(self.core.text.offset_to_point(start).row);
let indent: String = self
.core
.text
.slice(line_start..start)
.chars()
.take_while(|c| *c == ' ' || *c == '\t')
.collect();
edits.push((range, format!("\n{indent}")));
}
let mut points: Vec<usize> = std::iter::once(self.core.selected_range.start)
.chain(self.extra_selections.iter().map(|sel| sel.start))
.collect();
self.apply_edits_and_track(&mut edits, &mut points, window, cx);
self.pause_blink_cursor(cx);
self.collapse_cursors(points, cx);
}
fn regroup_history(&mut self, before: usize) {
let pushed = self.core.history.undos().len().saturating_sub(before);
self.core.history.regroup_last(pushed);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::AppContext as _;
use crate::input_ui::{AddCursorBelow, Undo};
use crate::{Entity, Window};
struct Probe {
state: Entity<InputState>,
}
impl crate::Render for Probe {
fn render(
&mut self,
_window: &mut Window,
_cx: &mut Context<Self>,
) -> impl crate::IntoElement {
crate::div()
}
}
struct InputProbe {
state: Entity<InputState>,
}
impl crate::Render for InputProbe {
fn render(
&mut self,
window: &mut Window,
cx: &mut Context<Self>,
) -> impl crate::IntoElement {
use crate::{IntoElement as _, RenderOnce as _};
crate::input_ui::Input::new(&self.state)
.render(window, cx)
.into_element()
}
}
#[rgpui::test]
fn add_cursors_below_and_type_everywhere(cx: &mut crate::TestAppContext) {
cx.update(crate::input_ui::init);
cx.update(crate::theme::init);
let (probe, cx) = cx.add_window_view(|window, cx| {
let state = cx.new(|cx| InputState::new(window, cx).multi_line(true));
state.update(cx, |state, cx| state.set_value("a\nb\nc", window, cx));
Probe { state }
});
let state = probe.read_with(cx, |probe, _| probe.state.clone());
cx.update(|_, cx| {
state.update(cx, |state, cx| state.set_selected_range(0..0, cx));
});
cx.update(|window, cx| {
state.update(cx, |state, cx| {
state.add_cursor_below(&AddCursorBelow, window, cx)
});
state.update(cx, |state, cx| {
state.add_cursor_below(&AddCursorBelow, window, cx)
});
});
assert!(state.read_with(cx, |state, _| state.has_multiple_cursors()));
let extras: Vec<(usize, usize)> = state.read_with(cx, |state, _| {
state
.extra_selections()
.iter()
.map(|sel| (sel.start, sel.end))
.collect()
});
assert_eq!(extras, vec![(2, 2), (4, 4)]);
cx.update(|window, cx| {
state.update(cx, |state, cx| {
crate::EntityInputHandler::replace_text_in_range(state, None, "x", window, cx);
});
});
assert_eq!(
state.read_with(cx, |state, _| state.value().to_string()),
"xa\nxb\nxc".to_string()
);
cx.update(|window, cx| {
state.update(cx, |state, cx| state.undo(&Undo, window, cx));
});
assert_eq!(
state.read_with(cx, |state, _| state.value().to_string()),
"a\nb\nc".to_string()
);
}
#[rgpui::test]
fn escape_and_navigation_collapse_to_single(cx: &mut crate::TestAppContext) {
cx.update(crate::input_ui::init);
cx.update(crate::theme::init);
let (probe, cx) = cx.add_window_view(|window, cx| {
let state = cx.new(|cx| InputState::new(window, cx).multi_line(true));
state.update(cx, |state, cx| state.replace("a\nb\nc", window, cx));
Probe { state }
});
let state = probe.read_with(cx, |probe, _| probe.state.clone());
cx.update(|_, cx| {
state.update(cx, |state, cx| state.set_selected_range(0..0, cx));
});
cx.update(|window, cx| {
state.update(cx, |state, cx| {
state.add_cursor_below(&AddCursorBelow, window, cx)
});
});
assert!(state.read_with(cx, |state, _| state.has_multiple_cursors()));
cx.update(|window, cx| {
state.update(cx, |state, cx| {
state.escape(&crate::input_ui::Escape, window, cx)
});
});
assert!(!state.read_with(cx, |state, _| state.has_multiple_cursors()));
cx.update(|window, cx| {
state.update(cx, |state, cx| {
state.add_cursor_below(&AddCursorBelow, window, cx)
});
});
cx.update(|window, cx| {
state.update(cx, |state, cx| {
state.right(&crate::input_ui::MoveRight, window, cx)
});
});
assert!(!state.read_with(cx, |state, _| state.has_multiple_cursors()));
}
#[rgpui::test]
fn extra_cursors_render_without_panic(cx: &mut crate::TestAppContext) {
cx.update(crate::input_ui::init);
cx.update(crate::theme::init);
let (probe, cx) = cx.add_window_view(|window, cx| InputProbe {
state: cx.new(|cx| {
let mut state = InputState::new(window, cx).multi_line(true);
state.replace("a\nb\nc", window, cx);
state
}),
});
let state = probe.read_with(cx, |probe, _| probe.state.clone());
cx.update(|_, cx| {
state.update(cx, |state, cx| state.set_selected_range(0..3, cx));
});
cx.update(|window, cx| {
state.update(cx, |state, cx| {
state.add_cursor_below(&AddCursorBelow, window, cx)
});
});
cx.update(|window, cx| {
_ = window.draw(cx);
});
assert!(state.read_with(cx, |state, _| state.has_multiple_cursors()));
}
}