Detailed changes
@@ -5,22 +5,24 @@ mod tab_map;
mod wrap_map;
use crate::{
- link_go_to_definition::InlayHighlight, Anchor, AnchorRangeExt, InlayId, MultiBuffer,
- MultiBufferSnapshot, ToOffset, ToPoint,
+ link_go_to_definition::InlayHighlight, movement::TextLayoutDetails, Anchor, AnchorRangeExt,
+ EditorStyle, InlayId, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint,
};
pub use block_map::{BlockMap, BlockPoint};
use collections::{BTreeMap, HashMap, HashSet};
use fold_map::FoldMap;
use gpui::{
color::Color,
- fonts::{FontId, HighlightStyle},
+ fonts::{FontId, HighlightStyle, Underline},
+ text_layout::{Line, RunStyle},
Entity, ModelContext, ModelHandle,
};
use inlay_map::InlayMap;
use language::{
language_settings::language_settings, OffsetUtf16, Point, Subscription as BufferSubscription,
};
-use std::{any::TypeId, fmt::Debug, num::NonZeroU32, ops::Range, sync::Arc};
+use lsp::DiagnosticSeverity;
+use std::{any::TypeId, borrow::Cow, fmt::Debug, num::NonZeroU32, ops::Range, sync::Arc};
use sum_tree::{Bias, TreeMap};
use tab_map::TabMap;
use wrap_map::WrapMap;
@@ -316,6 +318,12 @@ pub struct Highlights<'a> {
pub suggestion_highlight_style: Option<HighlightStyle>,
}
+pub struct HighlightedChunk<'a> {
+ pub chunk: &'a str,
+ pub style: Option<HighlightStyle>,
+ pub is_tab: bool,
+}
+
pub struct DisplaySnapshot {
pub buffer_snapshot: MultiBufferSnapshot,
pub fold_snapshot: fold_map::FoldSnapshot,
@@ -485,7 +493,7 @@ impl DisplaySnapshot {
language_aware: bool,
inlay_highlight_style: Option<HighlightStyle>,
suggestion_highlight_style: Option<HighlightStyle>,
- ) -> DisplayChunks<'_> {
+ ) -> DisplayChunks<'a> {
self.block_snapshot.chunks(
display_rows,
language_aware,
@@ -498,6 +506,140 @@ impl DisplaySnapshot {
)
}
+ pub fn highlighted_chunks<'a>(
+ &'a self,
+ display_rows: Range<u32>,
+ language_aware: bool,
+ style: &'a EditorStyle,
+ ) -> impl Iterator<Item = HighlightedChunk<'a>> {
+ self.chunks(
+ display_rows,
+ language_aware,
+ Some(style.theme.hint),
+ Some(style.theme.suggestion),
+ )
+ .map(|chunk| {
+ let mut highlight_style = chunk
+ .syntax_highlight_id
+ .and_then(|id| id.style(&style.syntax));
+
+ if let Some(chunk_highlight) = chunk.highlight_style {
+ if let Some(highlight_style) = highlight_style.as_mut() {
+ highlight_style.highlight(chunk_highlight);
+ } else {
+ highlight_style = Some(chunk_highlight);
+ }
+ }
+
+ let mut diagnostic_highlight = HighlightStyle::default();
+
+ if chunk.is_unnecessary {
+ diagnostic_highlight.fade_out = Some(style.unnecessary_code_fade);
+ }
+
+ if let Some(severity) = chunk.diagnostic_severity {
+ // Omit underlines for HINT/INFO diagnostics on 'unnecessary' code.
+ if severity <= DiagnosticSeverity::WARNING || !chunk.is_unnecessary {
+ let diagnostic_style = super::diagnostic_style(severity, true, style);
+ diagnostic_highlight.underline = Some(Underline {
+ color: Some(diagnostic_style.message.text.color),
+ thickness: 1.0.into(),
+ squiggly: true,
+ });
+ }
+ }
+
+ if let Some(highlight_style) = highlight_style.as_mut() {
+ highlight_style.highlight(diagnostic_highlight);
+ } else {
+ highlight_style = Some(diagnostic_highlight);
+ }
+
+ HighlightedChunk {
+ chunk: chunk.text,
+ style: highlight_style,
+ is_tab: chunk.is_tab,
+ }
+ })
+ }
+
+ pub fn lay_out_line_for_row(
+ &self,
+ display_row: u32,
+ TextLayoutDetails {
+ font_cache,
+ text_layout_cache,
+ editor_style,
+ }: &TextLayoutDetails,
+ ) -> Line {
+ let mut styles = Vec::new();
+ let mut line = String::new();
+ let mut ended_in_newline = false;
+
+ let range = display_row..display_row + 1;
+ for chunk in self.highlighted_chunks(range, false, editor_style) {
+ line.push_str(chunk.chunk);
+
+ let text_style = if let Some(style) = chunk.style {
+ editor_style
+ .text
+ .clone()
+ .highlight(style, font_cache)
+ .map(Cow::Owned)
+ .unwrap_or_else(|_| Cow::Borrowed(&editor_style.text))
+ } else {
+ Cow::Borrowed(&editor_style.text)
+ };
+ ended_in_newline = chunk.chunk.ends_with("\n");
+
+ styles.push((
+ chunk.chunk.len(),
+ RunStyle {
+ font_id: text_style.font_id,
+ color: text_style.color,
+ underline: text_style.underline,
+ },
+ ));
+ }
+
+ // our pixel positioning logic assumes each line ends in \n,
+ // this is almost always true except for the last line which
+ // may have no trailing newline.
+ if !ended_in_newline && display_row == self.max_point().row() {
+ line.push_str("\n");
+
+ styles.push((
+ "\n".len(),
+ RunStyle {
+ font_id: editor_style.text.font_id,
+ color: editor_style.text_color,
+ underline: editor_style.text.underline,
+ },
+ ));
+ }
+
+ text_layout_cache.layout_str(&line, editor_style.text.font_size, &styles)
+ }
+
+ pub fn x_for_point(
+ &self,
+ display_point: DisplayPoint,
+ text_layout_details: &TextLayoutDetails,
+ ) -> f32 {
+ let layout_line = self.lay_out_line_for_row(display_point.row(), text_layout_details);
+ layout_line.x_for_index(display_point.column() as usize)
+ }
+
+ pub fn column_for_x(
+ &self,
+ display_row: u32,
+ x_coordinate: f32,
+ text_layout_details: &TextLayoutDetails,
+ ) -> u32 {
+ let layout_line = self.lay_out_line_for_row(display_row, text_layout_details);
+ layout_line.closest_index_for_x(x_coordinate) as u32
+ }
+
pub fn chars_at(
&self,
mut point: DisplayPoint,
@@ -869,12 +1011,16 @@ pub fn next_rows(display_row: u32, display_map: &DisplaySnapshot) -> impl Iterat
#[cfg(test)]
pub mod tests {
use super::*;
- use crate::{movement, test::marked_display_snapshot};
+ use crate::{
+ movement,
+ test::{editor_test_context::EditorTestContext, marked_display_snapshot},
+ };
use gpui::{color::Color, elements::*, test::observe, AppContext};
use language::{
language_settings::{AllLanguageSettings, AllLanguageSettingsContent},
Buffer, Language, LanguageConfig, SelectionGoal,
};
+ use project::Project;
use rand::{prelude::*, Rng};
use settings::SettingsStore;
use smol::stream::StreamExt;
@@ -1148,95 +1294,120 @@ pub mod tests {
}
#[gpui::test(retries = 5)]
- fn test_soft_wraps(cx: &mut AppContext) {
+ async fn test_soft_wraps(cx: &mut gpui::TestAppContext) {
cx.foreground().set_block_on_ticks(usize::MAX..=usize::MAX);
- init_test(cx, |_| {});
+ cx.update(|cx| {
+ init_test(cx, |_| {});
+ });
- let font_cache = cx.font_cache();
+ let mut cx = EditorTestContext::new(cx).await;
+ let editor = cx.editor.clone();
+ let window = cx.window.clone();
- let family_id = font_cache
- .load_family(&["Helvetica"], &Default::default())
- .unwrap();
- let font_id = font_cache
- .select_font(family_id, &Default::default())
- .unwrap();
- let font_size = 12.0;
- let wrap_width = Some(64.);
+ cx.update_window(window, |cx| {
+ let text_layout_details =
+ editor.read_with(cx, |editor, cx| editor.text_layout_details(cx));
- let text = "one two three four five\nsix seven eight";
- let buffer = MultiBuffer::build_simple(text, cx);
- let map = cx.add_model(|cx| {
- DisplayMap::new(buffer.clone(), font_id, font_size, wrap_width, 1, 1, cx)
- });
+ let font_cache = cx.font_cache().clone();
- let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
- assert_eq!(
- snapshot.text_chunks(0).collect::<String>(),
- "one two \nthree four \nfive\nsix seven \neight"
- );
- assert_eq!(
- snapshot.clip_point(DisplayPoint::new(0, 8), Bias::Left),
- DisplayPoint::new(0, 7)
- );
- assert_eq!(
- snapshot.clip_point(DisplayPoint::new(0, 8), Bias::Right),
- DisplayPoint::new(1, 0)
- );
- assert_eq!(
- movement::right(&snapshot, DisplayPoint::new(0, 7)),
- DisplayPoint::new(1, 0)
- );
- assert_eq!(
- movement::left(&snapshot, DisplayPoint::new(1, 0)),
- DisplayPoint::new(0, 7)
- );
- assert_eq!(
- movement::up(
- &snapshot,
- DisplayPoint::new(1, 10),
- SelectionGoal::None,
- false
- ),
- (DisplayPoint::new(0, 7), SelectionGoal::Column(10))
- );
- assert_eq!(
- movement::down(
- &snapshot,
- DisplayPoint::new(0, 7),
- SelectionGoal::Column(10),
- false
- ),
- (DisplayPoint::new(1, 10), SelectionGoal::Column(10))
- );
- assert_eq!(
- movement::down(
- &snapshot,
- DisplayPoint::new(1, 10),
- SelectionGoal::Column(10),
- false
- ),
- (DisplayPoint::new(2, 4), SelectionGoal::Column(10))
- );
+ let family_id = font_cache
+ .load_family(&["Helvetica"], &Default::default())
+ .unwrap();
+ let font_id = font_cache
+ .select_font(family_id, &Default::default())
+ .unwrap();
+ let font_size = 12.0;
+ let wrap_width = Some(64.);
- let ix = snapshot.buffer_snapshot.text().find("seven").unwrap();
- buffer.update(cx, |buffer, cx| {
- buffer.edit([(ix..ix, "and ")], None, cx);
- });
+ let text = "one two three four five\nsix seven eight";
+ let buffer = MultiBuffer::build_simple(text, cx);
+ let map = cx.add_model(|cx| {
+ DisplayMap::new(buffer.clone(), font_id, font_size, wrap_width, 1, 1, cx)
+ });
- let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
- assert_eq!(
- snapshot.text_chunks(1).collect::<String>(),
- "three four \nfive\nsix and \nseven eight"
- );
+ let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
+ assert_eq!(
+ snapshot.text_chunks(0).collect::<String>(),
+ "one two \nthree four \nfive\nsix seven \neight"
+ );
+ assert_eq!(
+ snapshot.clip_point(DisplayPoint::new(0, 8), Bias::Left),
+ DisplayPoint::new(0, 7)
+ );
+ assert_eq!(
+ snapshot.clip_point(DisplayPoint::new(0, 8), Bias::Right),
+ DisplayPoint::new(1, 0)
+ );
+ assert_eq!(
+ movement::right(&snapshot, DisplayPoint::new(0, 7)),
+ DisplayPoint::new(1, 0)
+ );
+ assert_eq!(
+ movement::left(&snapshot, DisplayPoint::new(1, 0)),
+ DisplayPoint::new(0, 7)
+ );
- // Re-wrap on font size changes
- map.update(cx, |map, cx| map.set_font(font_id, font_size + 3., cx));
+ let x = snapshot.x_for_point(DisplayPoint::new(1, 10), &text_layout_details);
+ assert_eq!(
+ movement::up(
+ &snapshot,
+ DisplayPoint::new(1, 10),
+ SelectionGoal::None,
+ false,
+ &text_layout_details,
+ ),
+ (
+ DisplayPoint::new(0, 7),
+ SelectionGoal::HorizontalPosition(x)
+ )
+ );
+ assert_eq!(
+ movement::down(
+ &snapshot,
+ DisplayPoint::new(0, 7),
+ SelectionGoal::HorizontalPosition(x),
+ false,
+ &text_layout_details
+ ),
+ (
+ DisplayPoint::new(1, 10),
+ SelectionGoal::HorizontalPosition(x)
+ )
+ );
+ assert_eq!(
+ movement::down(
+ &snapshot,
+ DisplayPoint::new(1, 10),
+ SelectionGoal::HorizontalPosition(x),
+ false,
+ &text_layout_details
+ ),
+ (
+ DisplayPoint::new(2, 4),
+ SelectionGoal::HorizontalPosition(x)
+ )
+ );
- let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
- assert_eq!(
- snapshot.text_chunks(1).collect::<String>(),
- "three \nfour five\nsix and \nseven \neight"
- )
+ let ix = snapshot.buffer_snapshot.text().find("seven").unwrap();
+ buffer.update(cx, |buffer, cx| {
+ buffer.edit([(ix..ix, "and ")], None, cx);
+ });
+
+ let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
+ assert_eq!(
+ snapshot.text_chunks(1).collect::<String>(),
+ "three four \nfive\nsix and \nseven eight"
+ );
+
+ // Re-wrap on font size changes
+ map.update(cx, |map, cx| map.set_font(font_id, font_size + 3., cx));
+
+ let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
+ assert_eq!(
+ snapshot.text_chunks(1).collect::<String>(),
+ "three \nfour five\nsix and \nseven \neight"
+ )
+ });
}
#[gpui::test]
@@ -1731,6 +1902,9 @@ pub mod tests {
cx.foreground().forbid_parking();
cx.set_global(SettingsStore::test(cx));
language::init(cx);
+ crate::init(cx);
+ Project::init_settings(cx);
+ theme::init((), cx);
cx.update_global::<SettingsStore, _, _>(|store, cx| {
store.update_user_settings::<AllLanguageSettings>(cx, f);
});
@@ -71,6 +71,7 @@ use link_go_to_definition::{
};
use log::error;
use lsp::LanguageServerId;
+use movement::TextLayoutDetails;
use multi_buffer::ToOffsetUtf16;
pub use multi_buffer::{
Anchor, AnchorRangeExt, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, ToOffset,
@@ -3476,6 +3477,14 @@ impl Editor {
.collect()
}
+ pub fn text_layout_details(&self, cx: &WindowContext) -> TextLayoutDetails {
+ TextLayoutDetails {
+ font_cache: cx.font_cache().clone(),
+ text_layout_cache: cx.text_layout_cache().clone(),
+ editor_style: self.style(cx),
+ }
+ }
+
fn splice_inlay_hints(
&self,
to_remove: Vec<InlayId>,
@@ -5410,6 +5419,7 @@ impl Editor {
}
pub fn transpose(&mut self, _: &Transpose, cx: &mut ViewContext<Self>) {
+ let text_layout_details = &self.text_layout_details(cx);
self.transact(cx, |this, cx| {
let edits = this.change_selections(Some(Autoscroll::fit()), cx, |s| {
let mut edits: Vec<(Range<usize>, String)> = Default::default();
@@ -5433,7 +5443,10 @@ impl Editor {
*head.column_mut() += 1;
head = display_map.clip_point(head, Bias::Right);
- selection.collapse_to(head, SelectionGoal::Column(head.column()));
+ let goal = SelectionGoal::HorizontalPosition(
+ display_map.x_for_point(head, &text_layout_details),
+ );
+ selection.collapse_to(head, goal);
let transpose_start = display_map
.buffer_snapshot
@@ -5697,13 +5710,21 @@ impl Editor {
return;
}
+ let text_layout_details = &self.text_layout_details(cx);
+
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
let line_mode = s.line_mode;
s.move_with(|map, selection| {
if !selection.is_empty() && !line_mode {
selection.goal = SelectionGoal::None;
}
- let (cursor, goal) = movement::up(map, selection.start, selection.goal, false);
+ let (cursor, goal) = movement::up(
+ map,
+ selection.start,
+ selection.goal,
+ false,
+ &text_layout_details,
+ );
selection.collapse_to(cursor, goal);
});
})
@@ -5731,22 +5752,33 @@ impl Editor {
Autoscroll::fit()
};
+ let text_layout_details = &self.text_layout_details(cx);
+
self.change_selections(Some(autoscroll), cx, |s| {
let line_mode = s.line_mode;
s.move_with(|map, selection| {
if !selection.is_empty() && !line_mode {
selection.goal = SelectionGoal::None;
}
- let (cursor, goal) =
- movement::up_by_rows(map, selection.end, row_count, selection.goal, false);
+ let (cursor, goal) = movement::up_by_rows(
+ map,
+ selection.end,
+ row_count,
+ selection.goal,
+ false,
+ &text_layout_details,
+ );
selection.collapse_to(cursor, goal);
});
});
}
pub fn select_up(&mut self, _: &SelectUp, cx: &mut ViewContext<Self>) {
+ let text_layout_details = &self.text_layout_details(cx);
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
- s.move_heads_with(|map, head, goal| movement::up(map, head, goal, false))
+ s.move_heads_with(|map, head, goal| {
+ movement::up(map, head, goal, false, &text_layout_details)
+ })
})
}
@@ -5758,13 +5790,20 @@ impl Editor {
return;
}
+ let text_layout_details = &self.text_layout_details(cx);
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
let line_mode = s.line_mode;
s.move_with(|map, selection| {
if !selection.is_empty() && !line_mode {
selection.goal = SelectionGoal::None;
}
- let (cursor, goal) = movement::down(map, selection.end, selection.goal, false);
+ let (cursor, goal) = movement::down(
+ map,
+ selection.end,
+ selection.goal,
+ false,
+ &text_layout_details,
+ );
selection.collapse_to(cursor, goal);
});
});
@@ -5802,22 +5841,32 @@ impl Editor {
Autoscroll::fit()
};
+ let text_layout_details = &self.text_layout_details(cx);
self.change_selections(Some(autoscroll), cx, |s| {
let line_mode = s.line_mode;
s.move_with(|map, selection| {
if !selection.is_empty() && !line_mode {
selection.goal = SelectionGoal::None;
}
- let (cursor, goal) =
- movement::down_by_rows(map, selection.end, row_count, selection.goal, false);
+ let (cursor, goal) = movement::down_by_rows(
+ map,
+ selection.end,
+ row_count,
+ selection.goal,
+ false,
+ &text_layout_details,
+ );
selection.collapse_to(cursor, goal);
});
});
}
pub fn select_down(&mut self, _: &SelectDown, cx: &mut ViewContext<Self>) {
+ let text_layout_details = &self.text_layout_details(cx);
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
- s.move_heads_with(|map, head, goal| movement::down(map, head, goal, false))
+ s.move_heads_with(|map, head, goal| {
+ movement::down(map, head, goal, false, &text_layout_details)
+ })
});
}
@@ -6336,11 +6385,14 @@ impl Editor {
fn add_selection(&mut self, above: bool, cx: &mut ViewContext<Self>) {
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
let mut selections = self.selections.all::<Point>(cx);
+ let text_layout_details = self.text_layout_details(cx);
let mut state = self.add_selections_state.take().unwrap_or_else(|| {
let oldest_selection = selections.iter().min_by_key(|s| s.id).unwrap().clone();
let range = oldest_selection.display_range(&display_map).sorted();
- let columns = cmp::min(range.start.column(), range.end.column())
- ..cmp::max(range.start.column(), range.end.column());
+
+ let start_x = display_map.x_for_point(range.start, &text_layout_details);
+ let end_x = display_map.x_for_point(range.end, &text_layout_details);
+ let positions = start_x.min(end_x)..start_x.max(end_x);
selections.clear();
let mut stack = Vec::new();
@@ -6348,8 +6400,9 @@ impl Editor {
if let Some(selection) = self.selections.build_columnar_selection(
&display_map,
row,
- &columns,
+ &positions,
oldest_selection.reversed,
+ &text_layout_details,
) {
stack.push(selection.id);
selections.push(selection);
@@ -6377,12 +6430,15 @@ impl Editor {
let range = selection.display_range(&display_map).sorted();
debug_assert_eq!(range.start.row(), range.end.row());
let mut row = range.start.row();
- let columns = if let SelectionGoal::ColumnRange { start, end } = selection.goal
+ let positions = if let SelectionGoal::HorizontalRange { start, end } =
+ selection.goal
{
start..end
} else {
- cmp::min(range.start.column(), range.end.column())
- ..cmp::max(range.start.column(), range.end.column())
+ let start_x = display_map.x_for_point(range.start, &text_layout_details);
+ let end_x = display_map.x_for_point(range.end, &text_layout_details);
+
+ start_x.min(end_x)..start_x.max(end_x)
};
while row != end_row {
@@ -6395,8 +6451,9 @@ impl Editor {
if let Some(new_selection) = self.selections.build_columnar_selection(
&display_map,
row,
- &columns,
+ &positions,
selection.reversed,
+ &text_layout_details,
) {
state.stack.push(new_selection.id);
if above {
@@ -6690,6 +6747,7 @@ impl Editor {
}
pub fn toggle_comments(&mut self, action: &ToggleComments, cx: &mut ViewContext<Self>) {
+ let text_layout_details = &self.text_layout_details(cx);
self.transact(cx, |this, cx| {
let mut selections = this.selections.all::<Point>(cx);
let mut edits = Vec::new();
@@ -6932,7 +6990,10 @@ impl Editor {
point.row += 1;
point = snapshot.clip_point(point, Bias::Left);
let display_point = point.to_display_point(display_snapshot);
- (display_point, SelectionGoal::Column(display_point.column()))
+ let goal = SelectionGoal::HorizontalPosition(
+ display_snapshot.x_for_point(display_point, &text_layout_details),
+ );
+ (display_point, goal)
})
});
}
@@ -851,7 +851,7 @@ fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
let view = cx
.add_window(|cx| {
- let buffer = MultiBuffer::build_simple("โโโโโ\nabcde\nฮฑฮฒฮณฮดฮต\n", cx);
+ let buffer = MultiBuffer::build_simple("โโโโโ\nabcde\nฮฑฮฒฮณฮดฮต", cx);
build_editor(buffer.clone(), cx)
})
.root(cx);
@@ -869,7 +869,7 @@ fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
true,
cx,
);
- assert_eq!(view.display_text(cx), "โโโฏโ\nabโฏe\nฮฑฮฒโฏฮต\n");
+ assert_eq!(view.display_text(cx), "โโโฏโ\nabโฏe\nฮฑฮฒโฏฮต");
view.move_right(&MoveRight, cx);
assert_eq!(
@@ -888,6 +888,11 @@ fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
);
view.move_down(&MoveDown, cx);
+ assert_eq!(
+ view.selections.display_ranges(cx),
+ &[empty_range(1, "abโฏe".len())]
+ );
+ view.move_left(&MoveLeft, cx);
assert_eq!(
view.selections.display_ranges(cx),
&[empty_range(1, "abโฏ".len())]
@@ -929,17 +934,18 @@ fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
view.selections.display_ranges(cx),
&[empty_range(1, "abโฏe".len())]
);
- view.move_up(&MoveUp, cx);
+ view.move_down(&MoveDown, cx);
assert_eq!(
view.selections.display_ranges(cx),
- &[empty_range(0, "โโโฏโ".len())]
+ &[empty_range(2, "ฮฑฮฒโฏฮต".len())]
);
- view.move_left(&MoveLeft, cx);
+ view.move_up(&MoveUp, cx);
assert_eq!(
view.selections.display_ranges(cx),
- &[empty_range(0, "โโโฏ".len())]
+ &[empty_range(1, "abโฏe".len())]
);
- view.move_left(&MoveLeft, cx);
+
+ view.move_up(&MoveUp, cx);
assert_eq!(
view.selections.display_ranges(cx),
&[empty_range(0, "โโ".len())]
@@ -949,6 +955,11 @@ fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
view.selections.display_ranges(cx),
&[empty_range(0, "โ".len())]
);
+ view.move_left(&MoveLeft, cx);
+ assert_eq!(
+ view.selections.display_ranges(cx),
+ &[empty_range(0, "".len())]
+ );
});
}
@@ -4,7 +4,7 @@ use super::{
MAX_LINE_LEN,
};
use crate::{
- display_map::{BlockStyle, DisplaySnapshot, FoldStatus, TransformBlock},
+ display_map::{BlockStyle, DisplaySnapshot, FoldStatus, HighlightedChunk, TransformBlock},
editor_settings::ShowScrollbar,
git::{diff_hunk_to_display, DisplayDiffHunk},
hover_popover::{
@@ -22,7 +22,7 @@ use git::diff::DiffHunkStatus;
use gpui::{
color::Color,
elements::*,
- fonts::{HighlightStyle, TextStyle, Underline},
+ fonts::TextStyle,
geometry::{
rect::RectF,
vector::{vec2f, Vector2F},
@@ -37,8 +37,7 @@ use gpui::{
use itertools::Itertools;
use json::json;
use language::{
- language_settings::ShowWhitespaceSetting, Bias, CursorShape, DiagnosticSeverity, OffsetUtf16,
- Selection,
+ language_settings::ShowWhitespaceSetting, Bias, CursorShape, OffsetUtf16, Selection,
};
use project::{
project_settings::{GitGutterSetting, ProjectSettings},
@@ -1584,56 +1583,7 @@ impl EditorElement {
.collect()
} else {
let style = &self.style;
- let chunks = snapshot
- .chunks(
- rows.clone(),
- true,
- Some(style.theme.hint),
- Some(style.theme.suggestion),
- )
- .map(|chunk| {
- let mut highlight_style = chunk
- .syntax_highlight_id
- .and_then(|id| id.style(&style.syntax));
-
- if let Some(chunk_highlight) = chunk.highlight_style {
- if let Some(highlight_style) = highlight_style.as_mut() {
- highlight_style.highlight(chunk_highlight);
- } else {
- highlight_style = Some(chunk_highlight);
- }
- }
-
- let mut diagnostic_highlight = HighlightStyle::default();
-
- if chunk.is_unnecessary {
- diagnostic_highlight.fade_out = Some(style.unnecessary_code_fade);
- }
-
- if let Some(severity) = chunk.diagnostic_severity {
- // Omit underlines for HINT/INFO diagnostics on 'unnecessary' code.
- if severity <= DiagnosticSeverity::WARNING || !chunk.is_unnecessary {
- let diagnostic_style = super::diagnostic_style(severity, true, style);
- diagnostic_highlight.underline = Some(Underline {
- color: Some(diagnostic_style.message.text.color),
- thickness: 1.0.into(),
- squiggly: true,
- });
- }
- }
-
- if let Some(highlight_style) = highlight_style.as_mut() {
- highlight_style.highlight(diagnostic_highlight);
- } else {
- highlight_style = Some(diagnostic_highlight);
- }
-
- HighlightedChunk {
- chunk: chunk.text,
- style: highlight_style,
- is_tab: chunk.is_tab,
- }
- });
+ let chunks = snapshot.highlighted_chunks(rows.clone(), true, style);
LineWithInvisibles::from_chunks(
chunks,
@@ -1870,12 +1820,6 @@ impl EditorElement {
}
}
-struct HighlightedChunk<'a> {
- chunk: &'a str,
- style: Option<HighlightStyle>,
- is_tab: bool,
-}
-
#[derive(Debug)]
pub struct LineWithInvisibles {
pub line: Line,
@@ -1,7 +1,8 @@
use super::{Bias, DisplayPoint, DisplaySnapshot, SelectionGoal, ToDisplayPoint};
-use crate::{char_kind, CharKind, ToOffset, ToPoint};
+use crate::{char_kind, CharKind, EditorStyle, ToOffset, ToPoint};
+use gpui::{FontCache, TextLayoutCache};
use language::Point;
-use std::ops::Range;
+use std::{ops::Range, sync::Arc};
#[derive(Debug, PartialEq)]
pub enum FindRange {
@@ -9,6 +10,14 @@ pub enum FindRange {
MultiLine,
}
+/// TextLayoutDetails encompasses everything we need to move vertically
+/// taking into account variable width characters.
+pub struct TextLayoutDetails {
+ pub font_cache: Arc<FontCache>,
+ pub text_layout_cache: Arc<TextLayoutCache>,
+ pub editor_style: EditorStyle,
+}
+
pub fn left(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
if point.column() > 0 {
*point.column_mut() -= 1;
@@ -47,8 +56,16 @@ pub fn up(
start: DisplayPoint,
goal: SelectionGoal,
preserve_column_at_start: bool,
+ text_layout_details: &TextLayoutDetails,
) -> (DisplayPoint, SelectionGoal) {
- up_by_rows(map, start, 1, goal, preserve_column_at_start)
+ up_by_rows(
+ map,
+ start,
+ 1,
+ goal,
+ preserve_column_at_start,
+ text_layout_details,
+ )
}
pub fn down(
@@ -56,8 +73,16 @@ pub fn down(
start: DisplayPoint,
goal: SelectionGoal,
preserve_column_at_end: bool,
+ text_layout_details: &TextLayoutDetails,
) -> (DisplayPoint, SelectionGoal) {
- down_by_rows(map, start, 1, goal, preserve_column_at_end)
+ down_by_rows(
+ map,
+ start,
+ 1,
+ goal,
+ preserve_column_at_end,
+ text_layout_details,
+ )
}
pub fn up_by_rows(
@@ -66,11 +91,13 @@ pub fn up_by_rows(
row_count: u32,
goal: SelectionGoal,
preserve_column_at_start: bool,
+ text_layout_details: &TextLayoutDetails,
) -> (DisplayPoint, SelectionGoal) {
- let mut goal_column = match goal {
- SelectionGoal::Column(column) => column,
- SelectionGoal::ColumnRange { end, .. } => end,
- _ => map.column_to_chars(start.row(), start.column()),
+ let mut goal_x = match goal {
+ SelectionGoal::HorizontalPosition(x) => x,
+ SelectionGoal::WrappedHorizontalPosition((_, x)) => x,
+ SelectionGoal::HorizontalRange { end, .. } => end,
+ _ => map.x_for_point(start, text_layout_details),
};
let prev_row = start.row().saturating_sub(row_count);
@@ -79,19 +106,19 @@ pub fn up_by_rows(
Bias::Left,
);
if point.row() < start.row() {
- *point.column_mut() = map.column_from_chars(point.row(), goal_column);
+ *point.column_mut() = map.column_for_x(point.row(), goal_x, text_layout_details)
} else if preserve_column_at_start {
return (start, goal);
} else {
point = DisplayPoint::new(0, 0);
- goal_column = 0;
+ goal_x = 0.0;
}
let mut clipped_point = map.clip_point(point, Bias::Left);
if clipped_point.row() < point.row() {
clipped_point = map.clip_point(point, Bias::Right);
}
- (clipped_point, SelectionGoal::Column(goal_column))
+ (clipped_point, SelectionGoal::HorizontalPosition(goal_x))
}
pub fn down_by_rows(
@@ -100,29 +127,31 @@ pub fn down_by_rows(
row_count: u32,
goal: SelectionGoal,
preserve_column_at_end: bool,
+ text_layout_details: &TextLayoutDetails,
) -> (DisplayPoint, SelectionGoal) {
- let mut goal_column = match goal {
- SelectionGoal::Column(column) => column,
- SelectionGoal::ColumnRange { end, .. } => end,
- _ => map.column_to_chars(start.row(), start.column()),
+ let mut goal_x = match goal {
+ SelectionGoal::HorizontalPosition(x) => x,
+ SelectionGoal::WrappedHorizontalPosition((_, x)) => x,
+ SelectionGoal::HorizontalRange { end, .. } => end,
+ _ => map.x_for_point(start, text_layout_details),
};
let new_row = start.row() + row_count;
let mut point = map.clip_point(DisplayPoint::new(new_row, 0), Bias::Right);
if point.row() > start.row() {
- *point.column_mut() = map.column_from_chars(point.row(), goal_column);
+ *point.column_mut() = map.column_for_x(point.row(), goal_x, text_layout_details)
} else if preserve_column_at_end {
return (start, goal);
} else {
point = map.max_point();
- goal_column = map.column_to_chars(point.row(), point.column())
+ goal_x = map.x_for_point(point, text_layout_details)
}
let mut clipped_point = map.clip_point(point, Bias::Right);
if clipped_point.row() > point.row() {
clipped_point = map.clip_point(point, Bias::Left);
}
- (clipped_point, SelectionGoal::Column(goal_column))
+ (clipped_point, SelectionGoal::HorizontalPosition(goal_x))
}
pub fn line_beginning(
@@ -396,9 +425,11 @@ pub fn split_display_range_by_lines(
mod tests {
use super::*;
use crate::{
- display_map::Inlay, test::marked_display_snapshot, Buffer, DisplayMap, ExcerptRange,
- InlayId, MultiBuffer,
+ display_map::Inlay,
+ test::{editor_test_context::EditorTestContext, marked_display_snapshot},
+ Buffer, DisplayMap, ExcerptRange, InlayId, MultiBuffer,
};
+ use project::Project;
use settings::SettingsStore;
use util::post_inc;
@@ -691,123 +722,173 @@ mod tests {
}
#[gpui::test]
- fn test_move_up_and_down_with_excerpts(cx: &mut gpui::AppContext) {
- init_test(cx);
-
- let family_id = cx
- .font_cache()
- .load_family(&["Helvetica"], &Default::default())
- .unwrap();
- let font_id = cx
- .font_cache()
- .select_font(family_id, &Default::default())
- .unwrap();
+ async fn test_move_up_and_down_with_excerpts(cx: &mut gpui::TestAppContext) {
+ cx.update(|cx| {
+ init_test(cx);
+ });
- let buffer =
- cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "abc\ndefg\nhijkl\nmn"));
- let multibuffer = cx.add_model(|cx| {
- let mut multibuffer = MultiBuffer::new(0);
- multibuffer.push_excerpts(
- buffer.clone(),
- [
- ExcerptRange {
- context: Point::new(0, 0)..Point::new(1, 4),
- primary: None,
- },
- ExcerptRange {
- context: Point::new(2, 0)..Point::new(3, 2),
- primary: None,
- },
- ],
- cx,
+ let mut cx = EditorTestContext::new(cx).await;
+ let editor = cx.editor.clone();
+ let window = cx.window.clone();
+ cx.update_window(window, |cx| {
+ let text_layout_details =
+ editor.read_with(cx, |editor, cx| editor.text_layout_details(cx));
+
+ let family_id = cx
+ .font_cache()
+ .load_family(&["Helvetica"], &Default::default())
+ .unwrap();
+ let font_id = cx
+ .font_cache()
+ .select_font(family_id, &Default::default())
+ .unwrap();
+
+ let buffer =
+ cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "abc\ndefg\nhijkl\nmn"));
+ let multibuffer = cx.add_model(|cx| {
+ let mut multibuffer = MultiBuffer::new(0);
+ multibuffer.push_excerpts(
+ buffer.clone(),
+ [
+ ExcerptRange {
+ context: Point::new(0, 0)..Point::new(1, 4),
+ primary: None,
+ },
+ ExcerptRange {
+ context: Point::new(2, 0)..Point::new(3, 2),
+ primary: None,
+ },
+ ],
+ cx,
+ );
+ multibuffer
+ });
+ let display_map =
+ cx.add_model(|cx| DisplayMap::new(multibuffer, font_id, 14.0, None, 2, 2, cx));
+ let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
+
+ assert_eq!(snapshot.text(), "\n\nabc\ndefg\n\n\nhijkl\nmn");
+
+ let col_2_x = snapshot.x_for_point(DisplayPoint::new(2, 2), &text_layout_details);
+
+ // Can't move up into the first excerpt's header
+ assert_eq!(
+ up(
+ &snapshot,
+ DisplayPoint::new(2, 2),
+ SelectionGoal::HorizontalPosition(col_2_x),
+ false,
+ &text_layout_details
+ ),
+ (
+ DisplayPoint::new(2, 0),
+ SelectionGoal::HorizontalPosition(0.0)
+ ),
+ );
+ assert_eq!(
+ up(
+ &snapshot,
+ DisplayPoint::new(2, 0),
+ SelectionGoal::None,
+ false,
+ &text_layout_details
+ ),
+ (
+ DisplayPoint::new(2, 0),
+ SelectionGoal::HorizontalPosition(0.0)
+ ),
);
- multibuffer
- });
- let display_map =
- cx.add_model(|cx| DisplayMap::new(multibuffer, font_id, 14.0, None, 2, 2, cx));
- let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
- assert_eq!(snapshot.text(), "\n\nabc\ndefg\n\n\nhijkl\nmn");
+ let col_4_x = snapshot.x_for_point(DisplayPoint::new(3, 4), &text_layout_details);
- // Can't move up into the first excerpt's header
- assert_eq!(
- up(
- &snapshot,
- DisplayPoint::new(2, 2),
- SelectionGoal::Column(2),
- false
- ),
- (DisplayPoint::new(2, 0), SelectionGoal::Column(0)),
- );
- assert_eq!(
- up(
- &snapshot,
- DisplayPoint::new(2, 0),
- SelectionGoal::None,
- false
- ),
- (DisplayPoint::new(2, 0), SelectionGoal::Column(0)),
- );
+ // Move up and down within first excerpt
+ assert_eq!(
+ up(
+ &snapshot,
+ DisplayPoint::new(3, 4),
+ SelectionGoal::HorizontalPosition(col_4_x),
+ false,
+ &text_layout_details
+ ),
+ (
+ DisplayPoint::new(2, 3),
+ SelectionGoal::HorizontalPosition(col_4_x)
+ ),
+ );
+ assert_eq!(
+ down(
+ &snapshot,
+ DisplayPoint::new(2, 3),
+ SelectionGoal::HorizontalPosition(col_4_x),
+ false,
+ &text_layout_details
+ ),
+ (
+ DisplayPoint::new(3, 4),
+ SelectionGoal::HorizontalPosition(col_4_x)
+ ),
+ );
- // Move up and down within first excerpt
- assert_eq!(
- up(
- &snapshot,
- DisplayPoint::new(3, 4),
- SelectionGoal::Column(4),
- false
- ),
- (DisplayPoint::new(2, 3), SelectionGoal::Column(4)),
- );
- assert_eq!(
- down(
- &snapshot,
- DisplayPoint::new(2, 3),
- SelectionGoal::Column(4),
- false
- ),
- (DisplayPoint::new(3, 4), SelectionGoal::Column(4)),
- );
+ let col_5_x = snapshot.x_for_point(DisplayPoint::new(6, 5), &text_layout_details);
- // Move up and down across second excerpt's header
- assert_eq!(
- up(
- &snapshot,
- DisplayPoint::new(6, 5),
- SelectionGoal::Column(5),
- false
- ),
- (DisplayPoint::new(3, 4), SelectionGoal::Column(5)),
- );
- assert_eq!(
- down(
- &snapshot,
- DisplayPoint::new(3, 4),
- SelectionGoal::Column(5),
- false
- ),
- (DisplayPoint::new(6, 5), SelectionGoal::Column(5)),
- );
+ // Move up and down across second excerpt's header
+ assert_eq!(
+ up(
+ &snapshot,
+ DisplayPoint::new(6, 5),
+ SelectionGoal::HorizontalPosition(col_5_x),
+ false,
+ &text_layout_details
+ ),
+ (
+ DisplayPoint::new(3, 4),
+ SelectionGoal::HorizontalPosition(col_5_x)
+ ),
+ );
+ assert_eq!(
+ down(
+ &snapshot,
+ DisplayPoint::new(3, 4),
+ SelectionGoal::HorizontalPosition(col_5_x),
+ false,
+ &text_layout_details
+ ),
+ (
+ DisplayPoint::new(6, 5),
+ SelectionGoal::HorizontalPosition(col_5_x)
+ ),
+ );
- // Can't move down off the end
- assert_eq!(
- down(
- &snapshot,
- DisplayPoint::new(7, 0),
- SelectionGoal::Column(0),
- false
- ),
- (DisplayPoint::new(7, 2), SelectionGoal::Column(2)),
- );
- assert_eq!(
- down(
- &snapshot,
- DisplayPoint::new(7, 2),
- SelectionGoal::Column(2),
- false
- ),
- (DisplayPoint::new(7, 2), SelectionGoal::Column(2)),
- );
+ let max_point_x = snapshot.x_for_point(DisplayPoint::new(7, 2), &text_layout_details);
+
+ // Can't move down off the end
+ assert_eq!(
+ down(
+ &snapshot,
+ DisplayPoint::new(7, 0),
+ SelectionGoal::HorizontalPosition(0.0),
+ false,
+ &text_layout_details
+ ),
+ (
+ DisplayPoint::new(7, 2),
+ SelectionGoal::HorizontalPosition(max_point_x)
+ ),
+ );
+ assert_eq!(
+ down(
+ &snapshot,
+ DisplayPoint::new(7, 2),
+ SelectionGoal::HorizontalPosition(max_point_x),
+ false,
+ &text_layout_details
+ ),
+ (
+ DisplayPoint::new(7, 2),
+ SelectionGoal::HorizontalPosition(max_point_x)
+ ),
+ );
+ });
}
fn init_test(cx: &mut gpui::AppContext) {
@@ -815,5 +896,6 @@ mod tests {
theme::init((), cx);
language::init(cx);
crate::init(cx);
+ Project::init_settings(cx);
}
}
@@ -1,6 +1,6 @@
use std::{
cell::Ref,
- cmp, iter, mem,
+ iter, mem,
ops::{Deref, DerefMut, Range, Sub},
sync::Arc,
};
@@ -13,6 +13,7 @@ use util::post_inc;
use crate::{
display_map::{DisplayMap, DisplaySnapshot, ToDisplayPoint},
+ movement::TextLayoutDetails,
Anchor, DisplayPoint, ExcerptId, MultiBuffer, MultiBufferSnapshot, SelectMode, ToOffset,
};
@@ -305,23 +306,29 @@ impl SelectionsCollection {
&mut self,
display_map: &DisplaySnapshot,
row: u32,
- columns: &Range<u32>,
+ positions: &Range<f32>,
reversed: bool,
+ text_layout_details: &TextLayoutDetails,
) -> Option<Selection<Point>> {
- let is_empty = columns.start == columns.end;
+ let is_empty = positions.start == positions.end;
let line_len = display_map.line_len(row);
- if columns.start < line_len || (is_empty && columns.start == line_len) {
- let start = DisplayPoint::new(row, columns.start);
- let end = DisplayPoint::new(row, cmp::min(columns.end, line_len));
+
+ let layed_out_line = display_map.lay_out_line_for_row(row, &text_layout_details);
+
+ let start_col = layed_out_line.closest_index_for_x(positions.start) as u32;
+ if start_col < line_len || (is_empty && positions.start == layed_out_line.width()) {
+ let start = DisplayPoint::new(row, start_col);
+ let end_col = layed_out_line.closest_index_for_x(positions.end) as u32;
+ let end = DisplayPoint::new(row, end_col);
Some(Selection {
id: post_inc(&mut self.next_selection_id),
start: start.to_point(display_map),
end: end.to_point(display_map),
reversed,
- goal: SelectionGoal::ColumnRange {
- start: columns.start,
- end: columns.end,
+ goal: SelectionGoal::HorizontalRange {
+ start: positions.start,
+ end: positions.end,
},
})
} else {
@@ -266,6 +266,8 @@ impl Line {
self.layout.len == 0
}
+ /// index_for_x returns the character containing the given x coordinate.
+ /// (e.g. to handle a mouse-click)
pub fn index_for_x(&self, x: f32) -> Option<usize> {
if x >= self.layout.width {
None
@@ -281,6 +283,28 @@ impl Line {
}
}
+ /// closest_index_for_x returns the character boundary closest to the given x coordinate
+ /// (e.g. to handle aligning up/down arrow keys)
+ pub fn closest_index_for_x(&self, x: f32) -> usize {
+ let mut prev_index = 0;
+ let mut prev_x = 0.0;
+
+ for run in self.layout.runs.iter() {
+ for glyph in run.glyphs.iter() {
+ if glyph.position.x() >= x {
+ if glyph.position.x() - x < x - prev_x {
+ return glyph.index;
+ } else {
+ return prev_index;
+ }
+ }
+ prev_index = glyph.index;
+ prev_x = glyph.position.x();
+ }
+ }
+ prev_index
+ }
+
pub fn paint(
&self,
origin: Vector2F,
@@ -201,7 +201,7 @@ pub struct CodeAction {
pub lsp_action: lsp::CodeAction,
}
-#[derive(Clone, Debug, PartialEq, Eq)]
+#[derive(Clone, Debug, PartialEq)]
pub enum Operation {
Buffer(text::Operation),
@@ -224,7 +224,7 @@ pub enum Operation {
},
}
-#[derive(Clone, Debug, PartialEq, Eq)]
+#[derive(Clone, Debug, PartialEq)]
pub enum Event {
Operation(Operation),
Edited,
@@ -2,14 +2,15 @@ use crate::{Anchor, BufferSnapshot, TextDimension};
use std::cmp::Ordering;
use std::ops::Range;
-#[derive(Copy, Clone, Debug, Eq, PartialEq)]
+#[derive(Copy, Clone, Debug, PartialEq)]
pub enum SelectionGoal {
None,
- Column(u32),
- ColumnRange { start: u32, end: u32 },
+ HorizontalPosition(f32),
+ HorizontalRange { start: f32, end: f32 },
+ WrappedHorizontalPosition((u32, f32)),
}
-#[derive(Clone, Debug, Eq, PartialEq)]
+#[derive(Clone, Debug, PartialEq)]
pub struct Selection<T> {
pub id: usize,
pub start: T,
@@ -1,9 +1,7 @@
-use std::cmp;
-
use editor::{
char_kind,
display_map::{DisplaySnapshot, FoldPoint, ToDisplayPoint},
- movement::{self, find_boundary, find_preceding_boundary, FindRange},
+ movement::{self, find_boundary, find_preceding_boundary, FindRange, TextLayoutDetails},
Bias, CharKind, DisplayPoint, ToOffset,
};
use gpui::{actions, impl_actions, AppContext, WindowContext};
@@ -361,6 +359,7 @@ impl Motion {
point: DisplayPoint,
goal: SelectionGoal,
maybe_times: Option<usize>,
+ text_layout_details: &TextLayoutDetails,
) -> Option<(DisplayPoint, SelectionGoal)> {
let times = maybe_times.unwrap_or(1);
use Motion::*;
@@ -370,16 +369,16 @@ impl Motion {
Backspace => (backspace(map, point, times), SelectionGoal::None),
Down {
display_lines: false,
- } => down(map, point, goal, times),
+ } => up_down_buffer_rows(map, point, goal, times as isize, &text_layout_details),
Down {
display_lines: true,
- } => down_display(map, point, goal, times),
+ } => down_display(map, point, goal, times, &text_layout_details),
Up {
display_lines: false,
- } => up(map, point, goal, times),
+ } => up_down_buffer_rows(map, point, goal, 0 - times as isize, &text_layout_details),
Up {
display_lines: true,
- } => up_display(map, point, goal, times),
+ } => up_display(map, point, goal, times, &text_layout_details),
Right => (right(map, point, times), SelectionGoal::None),
NextWordStart { ignore_punctuation } => (
next_word_start(map, point, *ignore_punctuation, times),
@@ -442,10 +441,15 @@ impl Motion {
selection: &mut Selection<DisplayPoint>,
times: Option<usize>,
expand_to_surrounding_newline: bool,
+ text_layout_details: &TextLayoutDetails,
) -> bool {
- if let Some((new_head, goal)) =
- self.move_point(map, selection.head(), selection.goal, times)
- {
+ if let Some((new_head, goal)) = self.move_point(
+ map,
+ selection.head(),
+ selection.goal,
+ times,
+ &text_layout_details,
+ ) {
selection.set_head(new_head, goal);
if self.linewise() {
@@ -530,35 +534,85 @@ fn backspace(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> Di
point
}
-fn down(
+pub(crate) fn start_of_relative_buffer_row(
+ map: &DisplaySnapshot,
+ point: DisplayPoint,
+ times: isize,
+) -> DisplayPoint {
+ let start = map.display_point_to_fold_point(point, Bias::Left);
+ let target = start.row() as isize + times;
+ let new_row = (target.max(0) as u32).min(map.fold_snapshot.max_point().row());
+
+ map.clip_point(
+ map.fold_point_to_display_point(
+ map.fold_snapshot
+ .clip_point(FoldPoint::new(new_row, 0), Bias::Right),
+ ),
+ Bias::Right,
+ )
+}
+
+fn up_down_buffer_rows(
map: &DisplaySnapshot,
point: DisplayPoint,
mut goal: SelectionGoal,
- times: usize,
+ times: isize,
+ text_layout_details: &TextLayoutDetails,
) -> (DisplayPoint, SelectionGoal) {
let start = map.display_point_to_fold_point(point, Bias::Left);
+ let begin_folded_line = map.fold_point_to_display_point(
+ map.fold_snapshot
+ .clip_point(FoldPoint::new(start.row(), 0), Bias::Left),
+ );
+ let select_nth_wrapped_row = point.row() - begin_folded_line.row();
- let goal_column = match goal {
- SelectionGoal::Column(column) => column,
- SelectionGoal::ColumnRange { end, .. } => end,
+ let (goal_wrap, goal_x) = match goal {
+ SelectionGoal::WrappedHorizontalPosition((row, x)) => (row, x),
+ SelectionGoal::HorizontalRange { end, .. } => (select_nth_wrapped_row, end),
+ SelectionGoal::HorizontalPosition(x) => (select_nth_wrapped_row, x),
_ => {
- goal = SelectionGoal::Column(start.column());
- start.column()
+ let x = map.x_for_point(point, text_layout_details);
+ goal = SelectionGoal::WrappedHorizontalPosition((select_nth_wrapped_row, x));
+ (select_nth_wrapped_row, x)
}
};
- let new_row = cmp::min(
- start.row() + times as u32,
- map.fold_snapshot.max_point().row(),
- );
- let new_col = cmp::min(goal_column, map.fold_snapshot.line_len(new_row));
- let point = map.fold_point_to_display_point(
+ let target = start.row() as isize + times;
+ let new_row = (target.max(0) as u32).min(map.fold_snapshot.max_point().row());
+
+ let mut begin_folded_line = map.fold_point_to_display_point(
map.fold_snapshot
- .clip_point(FoldPoint::new(new_row, new_col), Bias::Left),
+ .clip_point(FoldPoint::new(new_row, 0), Bias::Left),
);
- // clip twice to "clip at end of line"
- (map.clip_point(point, Bias::Left), goal)
+ let mut i = 0;
+ while i < goal_wrap && begin_folded_line.row() < map.max_point().row() {
+ let next_folded_line = DisplayPoint::new(begin_folded_line.row() + 1, 0);
+ if map
+ .display_point_to_fold_point(next_folded_line, Bias::Right)
+ .row()
+ == new_row
+ {
+ i += 1;
+ begin_folded_line = next_folded_line;
+ } else {
+ break;
+ }
+ }
+
+ let new_col = if i == goal_wrap {
+ map.column_for_x(begin_folded_line.row(), goal_x, text_layout_details)
+ } else {
+ map.line_len(begin_folded_line.row())
+ };
+
+ (
+ map.clip_point(
+ DisplayPoint::new(begin_folded_line.row(), new_col),
+ Bias::Left,
+ ),
+ goal,
+ )
}
fn down_display(
@@ -566,49 +620,24 @@ fn down_display(
mut point: DisplayPoint,
mut goal: SelectionGoal,
times: usize,
+ text_layout_details: &TextLayoutDetails,
) -> (DisplayPoint, SelectionGoal) {
for _ in 0..times {
- (point, goal) = movement::down(map, point, goal, true);
+ (point, goal) = movement::down(map, point, goal, true, text_layout_details);
}
(point, goal)
}
-pub(crate) fn up(
- map: &DisplaySnapshot,
- point: DisplayPoint,
- mut goal: SelectionGoal,
- times: usize,
-) -> (DisplayPoint, SelectionGoal) {
- let start = map.display_point_to_fold_point(point, Bias::Left);
-
- let goal_column = match goal {
- SelectionGoal::Column(column) => column,
- SelectionGoal::ColumnRange { end, .. } => end,
- _ => {
- goal = SelectionGoal::Column(start.column());
- start.column()
- }
- };
-
- let new_row = start.row().saturating_sub(times as u32);
- let new_col = cmp::min(goal_column, map.fold_snapshot.line_len(new_row));
- let point = map.fold_point_to_display_point(
- map.fold_snapshot
- .clip_point(FoldPoint::new(new_row, new_col), Bias::Left),
- );
-
- (map.clip_point(point, Bias::Left), goal)
-}
-
fn up_display(
map: &DisplaySnapshot,
mut point: DisplayPoint,
mut goal: SelectionGoal,
times: usize,
+ text_layout_details: &TextLayoutDetails,
) -> (DisplayPoint, SelectionGoal) {
for _ in 0..times {
- (point, goal) = movement::up(map, point, goal, true);
+ (point, goal) = movement::up(map, point, goal, true, &text_layout_details);
}
(point, goal)
@@ -707,7 +736,7 @@ fn previous_word_start(
point
}
-fn first_non_whitespace(
+pub(crate) fn first_non_whitespace(
map: &DisplaySnapshot,
display_lines: bool,
from: DisplayPoint,
@@ -886,13 +915,17 @@ fn find_backward(
}
fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
- let correct_line = down(map, point, SelectionGoal::None, times).0;
+ let correct_line = start_of_relative_buffer_row(map, point, times as isize);
first_non_whitespace(map, false, correct_line)
}
-fn next_line_end(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
+pub(crate) fn next_line_end(
+ map: &DisplaySnapshot,
+ mut point: DisplayPoint,
+ times: usize,
+) -> DisplayPoint {
if times > 1 {
- point = down(map, point, SelectionGoal::None, times - 1).0;
+ point = start_of_relative_buffer_row(map, point, times as isize - 1);
}
end_of_line(map, false, point)
}
@@ -12,7 +12,7 @@ mod yank;
use std::sync::Arc;
use crate::{
- motion::{self, Motion},
+ motion::{self, first_non_whitespace, next_line_end, right, Motion},
object::Object,
state::{Mode, Operator},
Vim,
@@ -179,10 +179,11 @@ pub(crate) fn move_cursor(
cx: &mut WindowContext,
) {
vim.update_active_editor(cx, |editor, cx| {
+ let text_layout_details = editor.text_layout_details(cx);
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_cursors_with(|map, cursor, goal| {
motion
- .move_point(map, cursor, goal, times)
+ .move_point(map, cursor, goal, times, &text_layout_details)
.unwrap_or((cursor, goal))
})
})
@@ -195,9 +196,7 @@ fn insert_after(_: &mut Workspace, _: &InsertAfter, cx: &mut ViewContext<Workspa
vim.switch_mode(Mode::Insert, false, cx);
vim.update_active_editor(cx, |editor, cx| {
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
- s.maybe_move_cursors_with(|map, cursor, goal| {
- Motion::Right.move_point(map, cursor, goal, None)
- });
+ s.move_cursors_with(|map, cursor, _| (right(map, cursor, 1), SelectionGoal::None));
});
});
});
@@ -220,11 +219,11 @@ fn insert_first_non_whitespace(
vim.switch_mode(Mode::Insert, false, cx);
vim.update_active_editor(cx, |editor, cx| {
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
- s.maybe_move_cursors_with(|map, cursor, goal| {
- Motion::FirstNonWhitespace {
- display_lines: false,
- }
- .move_point(map, cursor, goal, None)
+ s.move_cursors_with(|map, cursor, _| {
+ (
+ first_non_whitespace(map, false, cursor),
+ SelectionGoal::None,
+ )
});
});
});
@@ -237,8 +236,8 @@ fn insert_end_of_line(_: &mut Workspace, _: &InsertEndOfLine, cx: &mut ViewConte
vim.switch_mode(Mode::Insert, false, cx);
vim.update_active_editor(cx, |editor, cx| {
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
- s.maybe_move_cursors_with(|map, cursor, goal| {
- Motion::CurrentLine.move_point(map, cursor, goal, None)
+ s.move_cursors_with(|map, cursor, _| {
+ (next_line_end(map, cursor, 1), SelectionGoal::None)
});
});
});
@@ -268,7 +267,7 @@ fn insert_line_above(_: &mut Workspace, _: &InsertLineAbove, cx: &mut ViewContex
editor.edit_with_autoindent(edits, cx);
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_cursors_with(|map, cursor, _| {
- let previous_line = motion::up(map, cursor, SelectionGoal::None, 1).0;
+ let previous_line = motion::start_of_relative_buffer_row(map, cursor, -1);
let insert_point = motion::end_of_line(map, false, previous_line);
(insert_point, SelectionGoal::None)
});
@@ -283,6 +282,7 @@ fn insert_line_below(_: &mut Workspace, _: &InsertLineBelow, cx: &mut ViewContex
vim.start_recording(cx);
vim.switch_mode(Mode::Insert, false, cx);
vim.update_active_editor(cx, |editor, cx| {
+ let text_layout_details = editor.text_layout_details(cx);
editor.transact(cx, |editor, cx| {
let (map, old_selections) = editor.selections.all_display(cx);
@@ -301,7 +301,13 @@ fn insert_line_below(_: &mut Workspace, _: &InsertLineBelow, cx: &mut ViewContex
});
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.maybe_move_cursors_with(|map, cursor, goal| {
- Motion::CurrentLine.move_point(map, cursor, goal, None)
+ Motion::CurrentLine.move_point(
+ map,
+ cursor,
+ goal,
+ None,
+ &text_layout_details,
+ )
});
});
editor.edit_with_autoindent(edits, cx);
@@ -399,12 +405,26 @@ mod test {
#[gpui::test]
async fn test_j(cx: &mut gpui::TestAppContext) {
- let mut cx = NeovimBackedTestContext::new(cx).await.binding(["j"]);
- cx.assert_all(indoc! {"
- หThe qหuick broหwn
- หfox jumps"
+ let mut cx = NeovimBackedTestContext::new(cx).await;
+
+ cx.set_shared_state(indoc! {"
+ aaหaa
+ ๐๐"
+ })
+ .await;
+ cx.simulate_shared_keystrokes(["j"]).await;
+ cx.assert_shared_state(indoc! {"
+ aaaa
+ ๐ห๐"
})
.await;
+
+ for marked_position in cx.each_marked_position(indoc! {"
+ หThe qหuick broหwn
+ หfox jumps"
+ }) {
+ cx.assert_neovim_compatible(&marked_position, ["j"]).await;
+ }
}
#[gpui::test]
@@ -2,7 +2,7 @@ use crate::{motion::Motion, object::Object, state::Mode, utils::copy_selections_
use editor::{
char_kind,
display_map::DisplaySnapshot,
- movement::{self, FindRange},
+ movement::{self, FindRange, TextLayoutDetails},
scroll::autoscroll::Autoscroll,
CharKind, DisplayPoint,
};
@@ -20,6 +20,7 @@ pub fn change_motion(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &m
| Motion::StartOfLine { .. }
);
vim.update_active_editor(cx, |editor, cx| {
+ let text_layout_details = editor.text_layout_details(cx);
editor.transact(cx, |editor, cx| {
// We are swapping to insert mode anyway. Just set the line end clipping behavior now
editor.set_clip_at_line_ends(false, cx);
@@ -27,9 +28,15 @@ pub fn change_motion(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &m
s.move_with(|map, selection| {
motion_succeeded |= if let Motion::NextWordStart { ignore_punctuation } = motion
{
- expand_changed_word_selection(map, selection, times, ignore_punctuation)
+ expand_changed_word_selection(
+ map,
+ selection,
+ times,
+ ignore_punctuation,
+ &text_layout_details,
+ )
} else {
- motion.expand_selection(map, selection, times, false)
+ motion.expand_selection(map, selection, times, false, &text_layout_details)
};
});
});
@@ -81,6 +88,7 @@ fn expand_changed_word_selection(
selection: &mut Selection<DisplayPoint>,
times: Option<usize>,
ignore_punctuation: bool,
+ text_layout_details: &TextLayoutDetails,
) -> bool {
if times.is_none() || times.unwrap() == 1 {
let scope = map
@@ -103,11 +111,22 @@ fn expand_changed_word_selection(
});
true
} else {
- Motion::NextWordStart { ignore_punctuation }
- .expand_selection(map, selection, None, false)
+ Motion::NextWordStart { ignore_punctuation }.expand_selection(
+ map,
+ selection,
+ None,
+ false,
+ &text_layout_details,
+ )
}
} else {
- Motion::NextWordStart { ignore_punctuation }.expand_selection(map, selection, times, false)
+ Motion::NextWordStart { ignore_punctuation }.expand_selection(
+ map,
+ selection,
+ times,
+ false,
+ &text_layout_details,
+ )
}
}
@@ -7,6 +7,7 @@ use language::Point;
pub fn delete_motion(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &mut WindowContext) {
vim.stop_recording();
vim.update_active_editor(cx, |editor, cx| {
+ let text_layout_details = editor.text_layout_details(cx);
editor.transact(cx, |editor, cx| {
editor.set_clip_at_line_ends(false, cx);
let mut original_columns: HashMap<_, _> = Default::default();
@@ -14,7 +15,7 @@ pub fn delete_motion(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &m
s.move_with(|map, selection| {
let original_head = selection.head();
original_columns.insert(selection.id, original_head.column());
- motion.expand_selection(map, selection, times, true);
+ motion.expand_selection(map, selection, times, true, &text_layout_details);
// Motion::NextWordStart on an empty line should delete it.
if let Motion::NextWordStart {
@@ -255,8 +255,18 @@ mod test {
4
5"})
.await;
- cx.simulate_shared_keystrokes(["shift-g", "ctrl-v", "g", "g", "g", "ctrl-x"])
+
+ cx.simulate_shared_keystrokes(["shift-g", "ctrl-v", "g", "g"])
+ .await;
+ cx.assert_shared_state(indoc! {"
+ ยซ1หยป
+ ยซ2หยป
+ ยซ3หยป 2
+ ยซ4หยป
+ ยซ5หยป"})
.await;
+
+ cx.simulate_shared_keystrokes(["g", "ctrl-x"]).await;
cx.assert_shared_state(indoc! {"
ห0
0
@@ -30,6 +30,7 @@ fn paste(_: &mut Workspace, action: &Paste, cx: &mut ViewContext<Workspace>) {
Vim::update(cx, |vim, cx| {
vim.record_current_action(cx);
vim.update_active_editor(cx, |editor, cx| {
+ let text_layout_details = editor.text_layout_details(cx);
editor.transact(cx, |editor, cx| {
editor.set_clip_at_line_ends(false, cx);
@@ -168,8 +169,14 @@ fn paste(_: &mut Workspace, action: &Paste, cx: &mut ViewContext<Workspace>) {
let mut cursor = anchor.to_display_point(map);
if *line_mode {
if !before {
- cursor =
- movement::down(map, cursor, SelectionGoal::None, false).0;
+ cursor = movement::down(
+ map,
+ cursor,
+ SelectionGoal::None,
+ false,
+ &text_layout_details,
+ )
+ .0;
}
cursor = movement::indented_line_beginning(map, cursor, true);
} else if !is_multiline {
@@ -32,10 +32,17 @@ pub fn substitute(vim: &mut Vim, count: Option<usize>, line_mode: bool, cx: &mut
vim.update_active_editor(cx, |editor, cx| {
editor.set_clip_at_line_ends(false, cx);
editor.transact(cx, |editor, cx| {
+ let text_layout_details = editor.text_layout_details(cx);
editor.change_selections(None, cx, |s| {
s.move_with(|map, selection| {
if selection.start == selection.end {
- Motion::Right.expand_selection(map, selection, count, true);
+ Motion::Right.expand_selection(
+ map,
+ selection,
+ count,
+ true,
+ &text_layout_details,
+ );
}
if line_mode {
// in Visual mode when the selection contains the newline at the end
@@ -43,7 +50,13 @@ pub fn substitute(vim: &mut Vim, count: Option<usize>, line_mode: bool, cx: &mut
if !selection.is_empty() && selection.end.column() == 0 {
selection.end = movement::left(map, selection.end);
}
- Motion::CurrentLine.expand_selection(map, selection, None, false);
+ Motion::CurrentLine.expand_selection(
+ map,
+ selection,
+ None,
+ false,
+ &text_layout_details,
+ );
if let Some((point, _)) = (Motion::FirstNonWhitespace {
display_lines: false,
})
@@ -52,6 +65,7 @@ pub fn substitute(vim: &mut Vim, count: Option<usize>, line_mode: bool, cx: &mut
selection.start,
selection.goal,
None,
+ &text_layout_details,
) {
selection.start = point;
}
@@ -4,6 +4,7 @@ use gpui::WindowContext;
pub fn yank_motion(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &mut WindowContext) {
vim.update_active_editor(cx, |editor, cx| {
+ let text_layout_details = editor.text_layout_details(cx);
editor.transact(cx, |editor, cx| {
editor.set_clip_at_line_ends(false, cx);
let mut original_positions: HashMap<_, _> = Default::default();
@@ -11,7 +12,7 @@ pub fn yank_motion(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &mut
s.move_with(|map, selection| {
let original_position = (selection.head(), selection.goal);
original_positions.insert(selection.id, original_position);
- motion.expand_selection(map, selection, times, true);
+ motion.expand_selection(map, selection, times, true, &text_layout_details);
});
});
copy_selections_content(editor, motion.linewise(), cx);
@@ -653,6 +653,63 @@ async fn test_selection_goal(cx: &mut gpui::TestAppContext) {
.await;
}
+#[gpui::test]
+async fn test_wrapped_motions(cx: &mut gpui::TestAppContext) {
+ let mut cx = NeovimBackedTestContext::new(cx).await;
+
+ cx.set_shared_wrap(12).await;
+
+ cx.set_shared_state(indoc! {"
+ aaหaa
+ ๐๐"
+ })
+ .await;
+ cx.simulate_shared_keystrokes(["j"]).await;
+ cx.assert_shared_state(indoc! {"
+ aaaa
+ ๐ห๐"
+ })
+ .await;
+
+ cx.set_shared_state(indoc! {"
+ 123456789012aaหaa
+ 123456789012๐๐"
+ })
+ .await;
+ cx.simulate_shared_keystrokes(["j"]).await;
+ cx.assert_shared_state(indoc! {"
+ 123456789012aaaa
+ 123456789012๐ห๐"
+ })
+ .await;
+
+ cx.set_shared_state(indoc! {"
+ 123456789012aaหaa
+ 123456789012๐๐"
+ })
+ .await;
+ cx.simulate_shared_keystrokes(["j"]).await;
+ cx.assert_shared_state(indoc! {"
+ 123456789012aaaa
+ 123456789012๐ห๐"
+ })
+ .await;
+
+ cx.set_shared_state(indoc! {"
+ 123456789012aaaaหaaaaaaaa123456789012
+ wow
+ 123456789012๐๐๐๐๐๐123456789012"
+ })
+ .await;
+ cx.simulate_shared_keystrokes(["j", "j"]).await;
+ cx.assert_shared_state(indoc! {"
+ 123456789012aaaaaaaaaaaa123456789012
+ wow
+ 123456789012๐๐ห๐๐๐๐123456789012"
+ })
+ .await;
+}
+
#[gpui::test]
async fn test_paragraphs_dont_wrap(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await;
@@ -590,7 +590,7 @@ impl Setting for VimModeSetting {
fn local_selections_changed(newest: Selection<usize>, cx: &mut WindowContext) {
Vim::update(cx, |vim, cx| {
if vim.enabled && vim.state().mode == Mode::Normal && !newest.is_empty() {
- if matches!(newest.goal, SelectionGoal::ColumnRange { .. }) {
+ if matches!(newest.goal, SelectionGoal::HorizontalRange { .. }) {
vim.switch_mode(Mode::VisualBlock, false, cx);
} else {
vim.switch_mode(Mode::Visual, false, cx)
@@ -57,6 +57,7 @@ pub fn init(cx: &mut AppContext) {
pub fn visual_motion(motion: Motion, times: Option<usize>, cx: &mut WindowContext) {
Vim::update(cx, |vim, cx| {
vim.update_active_editor(cx, |editor, cx| {
+ let text_layout_details = editor.text_layout_details(cx);
if vim.state().mode == Mode::VisualBlock
&& !matches!(
motion,
@@ -67,7 +68,7 @@ pub fn visual_motion(motion: Motion, times: Option<usize>, cx: &mut WindowContex
{
let is_up_or_down = matches!(motion, Motion::Up { .. } | Motion::Down { .. });
visual_block_motion(is_up_or_down, editor, cx, |map, point, goal| {
- motion.move_point(map, point, goal, times)
+ motion.move_point(map, point, goal, times, &text_layout_details)
})
} else {
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
@@ -89,9 +90,13 @@ pub fn visual_motion(motion: Motion, times: Option<usize>, cx: &mut WindowContex
current_head = movement::left(map, selection.end)
}
- let Some((new_head, goal)) =
- motion.move_point(map, current_head, selection.goal, times)
- else {
+ let Some((new_head, goal)) = motion.move_point(
+ map,
+ current_head,
+ selection.goal,
+ times,
+ &text_layout_details,
+ ) else {
return;
};
@@ -135,19 +140,23 @@ pub fn visual_block_motion(
SelectionGoal,
) -> Option<(DisplayPoint, SelectionGoal)>,
) {
+ let text_layout_details = editor.text_layout_details(cx);
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
let map = &s.display_map();
let mut head = s.newest_anchor().head().to_display_point(map);
let mut tail = s.oldest_anchor().tail().to_display_point(map);
+ let mut head_x = map.x_for_point(head, &text_layout_details);
+ let mut tail_x = map.x_for_point(tail, &text_layout_details);
+
let (start, end) = match s.newest_anchor().goal {
- SelectionGoal::ColumnRange { start, end } if preserve_goal => (start, end),
- SelectionGoal::Column(start) if preserve_goal => (start, start + 1),
- _ => (tail.column(), head.column()),
+ SelectionGoal::HorizontalRange { start, end } if preserve_goal => (start, end),
+ SelectionGoal::HorizontalPosition(start) if preserve_goal => (start, start),
+ _ => (tail_x, head_x),
};
- let goal = SelectionGoal::ColumnRange { start, end };
+ let mut goal = SelectionGoal::HorizontalRange { start, end };
- let was_reversed = tail.column() > head.column();
+ let was_reversed = tail_x > head_x;
if !was_reversed && !preserve_goal {
head = movement::saturating_left(map, head);
}
@@ -156,32 +165,56 @@ pub fn visual_block_motion(
return;
};
head = new_head;
+ head_x = map.x_for_point(head, &text_layout_details);
- let is_reversed = tail.column() > head.column();
+ let is_reversed = tail_x > head_x;
if was_reversed && !is_reversed {
- tail = movement::left(map, tail)
+ tail = movement::saturating_left(map, tail);
+ tail_x = map.x_for_point(tail, &text_layout_details);
} else if !was_reversed && is_reversed {
- tail = movement::right(map, tail)
+ tail = movement::saturating_right(map, tail);
+ tail_x = map.x_for_point(tail, &text_layout_details);
}
if !is_reversed && !preserve_goal {
- head = movement::saturating_right(map, head)
+ head = movement::saturating_right(map, head);
+ head_x = map.x_for_point(head, &text_layout_details);
}
- let columns = if is_reversed {
- head.column()..tail.column()
- } else if head.column() == tail.column() {
- head.column()..(head.column() + 1)
+ let positions = if is_reversed {
+ head_x..tail_x
} else {
- tail.column()..head.column()
+ tail_x..head_x
};
+ if !preserve_goal {
+ goal = SelectionGoal::HorizontalRange {
+ start: positions.start,
+ end: positions.end,
+ };
+ }
+
let mut selections = Vec::new();
let mut row = tail.row();
loop {
- let start = map.clip_point(DisplayPoint::new(row, columns.start), Bias::Left);
- let end = map.clip_point(DisplayPoint::new(row, columns.end), Bias::Left);
- if columns.start <= map.line_len(row) {
+ let layed_out_line = map.lay_out_line_for_row(row, &text_layout_details);
+ let start = DisplayPoint::new(
+ row,
+ layed_out_line.closest_index_for_x(positions.start) as u32,
+ );
+ let mut end = DisplayPoint::new(
+ row,
+ layed_out_line.closest_index_for_x(positions.end) as u32,
+ );
+ if end <= start {
+ if start.column() == map.line_len(start.row()) {
+ end = start;
+ } else {
+ end = movement::saturating_right(map, start);
+ }
+ }
+
+ if positions.start <= layed_out_line.width() {
let selection = Selection {
id: s.new_selection_id(),
start: start.to_point(map),
@@ -888,6 +921,28 @@ mod test {
.await;
}
+ #[gpui::test]
+ async fn test_visual_block_issue_2123(cx: &mut gpui::TestAppContext) {
+ let mut cx = NeovimBackedTestContext::new(cx).await;
+
+ cx.set_shared_state(indoc! {
+ "The หquick brown
+ fox jumps over
+ the lazy dog
+ "
+ })
+ .await;
+ cx.simulate_shared_keystrokes(["ctrl-v", "right", "down"])
+ .await;
+ cx.assert_shared_state(indoc! {
+ "The ยซquหยปick brown
+ fox ยซjuหยปmps over
+ the lazy dog
+ "
+ })
+ .await;
+ }
+
#[gpui::test]
async fn test_visual_block_insert(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await;
@@ -9,6 +9,7 @@
{"Key":"ctrl-v"}
{"Key":"g"}
{"Key":"g"}
+{"Get":{"state":"ยซ1หยป\nยซ2หยป\nยซ3หยป 2\nยซ4หยป\nยซ5หยป","mode":"VisualBlock"}}
{"Key":"g"}
{"Key":"ctrl-x"}
{"Get":{"state":"ห0\n0\n0 2\n0\n0","mode":"Normal"}}
@@ -1,3 +1,6 @@
+{"Put":{"state":"aaหaa\n๐๐"}}
+{"Key":"j"}
+{"Get":{"state":"aaaa\n๐ห๐","mode":"Normal"}}
{"Put":{"state":"หThe quick brown\nfox jumps"}}
{"Key":"j"}
{"Get":{"state":"The quick brown\nหfox jumps","mode":"Normal"}}
@@ -0,0 +1,5 @@
+{"Put":{"state":"The หquick brown\nfox jumps over\nthe lazy dog\n"}}
+{"Key":"ctrl-v"}
+{"Key":"right"}
+{"Key":"down"}
+{"Get":{"state":"The ยซquหยปick brown\nfox ยซjuหยปmps over\nthe lazy dog\n","mode":"VisualBlock"}}
@@ -0,0 +1,15 @@
+{"SetOption":{"value":"wrap"}}
+{"SetOption":{"value":"columns=12"}}
+{"Put":{"state":"aaหaa\n๐๐"}}
+{"Key":"j"}
+{"Get":{"state":"aaaa\n๐ห๐","mode":"Normal"}}
+{"Put":{"state":"123456789012aaหaa\n123456789012๐๐"}}
+{"Key":"j"}
+{"Get":{"state":"123456789012aaaa\n123456789012๐ห๐","mode":"Normal"}}
+{"Put":{"state":"123456789012aaหaa\n123456789012๐๐"}}
+{"Key":"j"}
+{"Get":{"state":"123456789012aaaa\n123456789012๐ห๐","mode":"Normal"}}
+{"Put":{"state":"123456789012aaaaหaaaaaaaa123456789012\nwow\n123456789012๐๐๐๐๐๐123456789012"}}
+{"Key":"j"}
+{"Key":"j"}
+{"Get":{"state":"123456789012aaaaaaaaaaaa123456789012\nwow\n123456789012๐๐ห๐๐๐๐123456789012","mode":"Normal"}}