Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

37 changes: 35 additions & 2 deletions crates/base/src/input/base/movement.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ impl<M: InputModeKind> InputBaseState<M> {

/// Like [`Self::preferred_column_for`], but resolves an offset on a soft wrap
/// boundary to the row the caret is drawn on.
fn preferred_column_for_with_affinity(
pub(super) fn preferred_column_for_with_affinity(
&self,
offset: usize,
line_end_affinity: bool,
Expand Down Expand Up @@ -138,6 +138,8 @@ impl<M: InputModeKind> InputBaseState<M> {
display_point.column = 0;
let mut new_offset = self.display_map.wrap_display_point_to_offset(display_point);

let column_anchor = column_anchor
.or_else(|| self.preferred_column_for_with_affinity(offset, line_end_affinity));
let mut new_affinity = false;
if let Some((preferred_x, column)) = column_anchor {
// Get display point again to update local_row.
Expand Down Expand Up @@ -172,6 +174,30 @@ impl<M: InputModeKind> InputBaseState<M> {
(new_offset, new_affinity)
}

/// Extend to the document edge when there is no further visual row. Plain
/// movement retains its column there, but selection must still reach the
/// remaining text on the first or last row.
pub(super) fn vertical_selection_target(
&self,
offset: usize,
column_anchor: Option<(Pixels, usize)>,
line_end_affinity: bool,
move_lines: isize,
) -> (usize, bool) {
let target = self.vertical_target(offset, column_anchor, line_end_affinity, move_lines);
if self.last_layout.is_some() {
let row = |offset, affinity| {
self.display_map
.offset_to_wrap_display_point_with_affinity(offset, affinity)
.row
};
if row(offset, line_end_affinity) == row(target.0, target.1) {
return (if move_lines < 0 { 0 } else { self.text.len() }, false);
}
}
target
}

/// Move every cursor through `f`, which maps each selection to a
/// `(new_offset, column_anchor, line_end_affinity)`, collapsing each to a
/// cursor. Overlapping cursors are merged, then the standard post-move
Expand Down Expand Up @@ -246,8 +272,15 @@ impl<M: InputModeKind> InputBaseState<M> {
(e, s.preferred_column_for(e), false)
} else {
let e = s.next_boundary(sel.end.saturating_sub(1));
(e, s.preferred_column_for(e), false)
let affinity = s.line_end_affinity_at(e);
(
e,
s.preferred_column_for_with_affinity(e, affinity),
affinity,
)
};
let anchor =
anchor.or_else(|| s.preferred_column_for_with_affinity(effective, affinity));
let (offset, affinity) = s.vertical_target(effective, anchor, affinity, move_lines);
(offset, anchor, affinity)
},
Expand Down
122 changes: 87 additions & 35 deletions crates/base/src/input/base/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1071,6 +1071,10 @@ impl<M: InputModeKind> InputBaseState<M> {
}

self.disabled = disabled;
if disabled {
M::hide_context_menu(self, cx);
M::clear_inline_completion(self, cx);
}
cx.notify();
}

Expand All @@ -1094,6 +1098,8 @@ impl<M: InputModeKind> InputBaseState<M> {

self.readonly = readonly;
if readonly {
M::hide_context_menu(self, cx);
M::clear_inline_completion(self, cx);
self.search_session.replace_mode = false;
}
cx.notify();
Expand Down Expand Up @@ -1315,14 +1321,16 @@ impl<M: InputModeKind> InputBaseState<M> {
if self.is_single_line() {
return;
}
self.undo_manager.break_transaction_coalescing();
self.select_all_cursors_to(
self.select_all_cursors_to_with_affinity(
|s, sel| {
let offset = s
.start_of_line_at(sel.cursor_offset(), s.line_end_affinity_for(sel))
.saturating_sub(1);
s.previous_boundary(offset)
s.vertical_selection_target(
sel.cursor_offset(),
sel.column_anchor,
s.line_end_affinity_for(sel),
-1,
)
},
true,
cx,
);
}
Expand All @@ -1331,15 +1339,16 @@ impl<M: InputModeKind> InputBaseState<M> {
if self.is_single_line() {
return;
}
self.undo_manager.break_transaction_coalescing();
let len = self.text.len();
self.select_all_cursors_to(
self.select_all_cursors_to_with_affinity(
|s, sel| {
let offset = (s.end_of_line_at(sel.cursor_offset(), s.line_end_affinity_for(sel))
+ 1)
.min(len);
s.next_boundary(offset)
s.vertical_selection_target(
sel.cursor_offset(),
sel.column_anchor,
s.line_end_affinity_for(sel),
1,
)
},
true,
cx,
);
}
Expand Down Expand Up @@ -1394,12 +1403,16 @@ impl<M: InputModeKind> InputBaseState<M> {
cx: &mut Context<Self>,
) {
self.undo_manager.break_transaction_coalescing();
self.select_all_cursors_to(
|s, sel| s.end_of_line_at(sel.cursor_offset(), s.line_end_affinity_for(sel)),
self.select_all_cursors_to_with_affinity(
|s, sel| {
(
s.end_of_line_at(sel.cursor_offset(), s.line_end_affinity_for(sel)),
true,
)
},
false,
cx,
);
// Mirrors MoveEnd: the caret belongs at the end of the visual row it is on.
self.cursor_line_end_affinity = true;
}

pub(super) fn select_to_previous_word(
Expand Down Expand Up @@ -2039,6 +2052,9 @@ impl<M: InputModeKind> InputBaseState<M> {
return;
}

// Escape also dismisses a request whose popup has not arrived yet.
M::hide_context_menu(self, cx);

// Collapse extra cursors back to the active one first.
if !self.selections.is_single() {
self.undo_manager.break_transaction_coalescing();
Expand All @@ -2052,6 +2068,7 @@ impl<M: InputModeKind> InputBaseState<M> {
M::clear_inline_completion(self, cx);
return; // Consume the escape, don't propagate
}
M::clear_inline_completion(self, cx);

// The handles and the edit menu are the topmost surface to dismiss.
if self.touch_selection().is_some() {
Expand Down Expand Up @@ -2187,8 +2204,9 @@ impl<M: InputModeKind> InputBaseState<M> {

self.undo_manager.break_transaction_coalescing();
let id = self.selections.generate_id();
self.selections
.add(CursorSelection::new(id, offset, offset));
let mut selection = CursorSelection::new(id, offset, offset);
selection.column_anchor = self.preferred_column_for(offset);
self.selections.add(selection);
cx.notify();
}

Expand Down Expand Up @@ -3016,20 +3034,24 @@ impl<M: InputModeKind> InputBaseState<M> {
last_line_pos = Some(pos);
}

// Mouse is below all visible lines, return end of text. A columnar selection
// still needs how far right the pointer was, so measure it against the last
// line rather than reporting a block that collapses at the bottom edge.
let columns_past_line_end = last_layout
// Clamp to the last laid-out row. Returning the end of the whole document
// would select unseen text before drag autoscroll has reached it.
let last_position = last_layout
.lines
.last()
.zip(last_line_pos)
.map(|(line_layout, pos)| {
.zip(last_layout.visible_line_byte_offsets.last())
.map(|((line_layout, pos), line_start)| {
let last_row_top = (line_layout.size(line_height).height - line_height).max(px(0.));
line_layout.columns_past_line_end(point(pos.x, last_row_top), last_layout)
})
.unwrap_or(0);
let pos = point(pos.x, last_row_top);
(
self.resolve_index(line_start + line_layout.len()),
false,
line_layout.columns_past_line_end(pos, last_layout),
)
});

(self.text.len(), false, columns_past_line_end)
last_position.unwrap_or((self.text.len(), false, 0))
}

/// Map a display byte index back to a text offset, undoing the mask expansion when the input
Expand Down Expand Up @@ -3115,32 +3137,53 @@ impl<M: InputModeKind> InputBaseState<M> {
&mut self,
f: impl Fn(&Self, &CursorSelection) -> usize,
cx: &mut Context<Self>,
) {
self.select_all_cursors_to_with_affinity(|s, sel| (f(s, sel), false), false, cx);
}

/// Extend selections with caret affinity, preserving their column anchors even
/// when vertical movement collapses a selection to a cursor.
fn select_all_cursors_to_with_affinity(
&mut self,
f: impl Fn(&Self, &CursorSelection) -> (usize, bool),
preserve_column: bool,
cx: &mut Context<Self>,
) {
self.pause_blink_cursor(cx);
self.undo_manager.break_transaction_coalescing();
M::clear_inline_completion(self, cx);

let mut active_affinity = false;
let new_selections: Vec<CursorSelection> = self
.selections
.iter()
.map(|sel| {
let offset = self.cursor_boundary(f(self, sel), Bias::Left);
let (offset, affinity) = f(self, sel);
if sel.id == self.active_selection().id {
active_affinity = affinity;
}
let offset = self.cursor_boundary(offset, Bias::Left);
let mut new_sel = *sel;
Self::extend_selection(&mut new_sel, offset, None);
let range = self.normalize_token_range(new_sel.start..new_sel.end);
new_sel.start = range.start;
new_sel.end = range.end;
if !preserve_column {
new_sel.column_anchor =
self.preferred_column_for_with_affinity(new_sel.cursor_offset(), affinity);
} else if new_sel.column_anchor.is_none() {
new_sel.column_anchor = self.preferred_column_for_with_affinity(
sel.cursor_offset(),
self.line_end_affinity_for(sel),
);
}
new_sel
})
.collect();
// Resolve targets using the old caret affinity before clearing it.
self.cursor_line_end_affinity = false;
self.cursor_line_end_affinity = active_affinity;
self.selections.replace_all(new_selections);
self.selections.merge_overlapping();

if self.active_selection().is_empty() {
self.update_preferred_column();
}
self.scroll_to(self.cursor(), None, cx);
cx.notify()
}
Expand Down Expand Up @@ -3249,6 +3292,7 @@ impl<M: InputModeKind> InputBaseState<M> {
return;
}

M::hide_context_menu(self, cx);
self.undo_manager.break_transaction_coalescing();

// NOTE: Do not cancel select, when blur.
Expand Down Expand Up @@ -3535,6 +3579,11 @@ impl<M: InputModeKind> InputBaseState<M> {
return;
}

// Every edit invalidates provider responses for the previous document,
// including deletion and indentation which do not trigger completion.
M::hide_context_menu(self, cx);
M::clear_inline_completion(self, cx);

// Sort descending by start so applying front-of-vec first edits the
// highest offsets first, leaving lower offsets unchanged.
let mut sorted: Vec<(Range<usize>, &str)> = edits
Expand Down Expand Up @@ -3771,7 +3820,7 @@ impl<M: InputModeKind> EntityInputHandler for InputBaseState<M> {
) -> Option<UTF16Selection> {
Some(UTF16Selection {
range: self.range_to_utf16(&self.selected_range()),
reversed: false,
reversed: self.active_selection().reversed,
})
}

Expand Down Expand Up @@ -4048,6 +4097,9 @@ impl<M: InputModeKind> EntityInputHandler for InputBaseState<M> {

let starts_composition = self.ime_marked_range.is_none();
if starts_composition {
// Even a canceled preedit separates the typing gestures on either
// side; its no-op transaction must not reconnect those gestures.
self.undo_manager.break_transaction_coalescing();
self.undo_manager.begin_transaction();
}

Expand Down
10 changes: 1 addition & 9 deletions crates/base/src/input/editor/lsp/completions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -255,15 +255,7 @@ impl InputBaseState<EditorMode> {
};
let handled = handler(kind, action, window, cx);
if handled && closes_overlay {
match kind {
super::InputOverlayKind::Completion => {
self.extras.context_menu_content.completion.open = false
}
super::InputOverlayKind::CodeAction => {
self.extras.context_menu_content.code_action.open = false
}
}
cx.notify();
self.hide_context_menu(cx);
}
handled
}
Expand Down
Loading
Loading