mirror of
https://github.com/zed-industries/zed.git
synced 2024-09-19 18:41:56 +03:00
pixel columns (#3052)
@ForLoveOfCats and I found a few speedups that make this acceptably fast (able to update ~10k selections in <100ms), so the remaining work here is to fix the tests, and then ship! Release notes: - Updated up/down to work based on pixel positions ([#1966](https://github.com/zed-industries/community/issues/1966)) ([#759](https://github.com/zed-industries/community/issues/759)) - vim: Fixed off-by-one in visual block mode ([2123](https://github.com/zed-industries/community/issues/2123))
This commit is contained in:
commit
0dae0f6027
@ -5,22 +5,24 @@ mod tab_map;
|
|||||||
mod wrap_map;
|
mod wrap_map;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
link_go_to_definition::InlayHighlight, Anchor, AnchorRangeExt, InlayId, MultiBuffer,
|
link_go_to_definition::InlayHighlight, movement::TextLayoutDetails, Anchor, AnchorRangeExt,
|
||||||
MultiBufferSnapshot, ToOffset, ToPoint,
|
EditorStyle, InlayId, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint,
|
||||||
};
|
};
|
||||||
pub use block_map::{BlockMap, BlockPoint};
|
pub use block_map::{BlockMap, BlockPoint};
|
||||||
use collections::{BTreeMap, HashMap, HashSet};
|
use collections::{BTreeMap, HashMap, HashSet};
|
||||||
use fold_map::FoldMap;
|
use fold_map::FoldMap;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
color::Color,
|
color::Color,
|
||||||
fonts::{FontId, HighlightStyle},
|
fonts::{FontId, HighlightStyle, Underline},
|
||||||
|
text_layout::{Line, RunStyle},
|
||||||
Entity, ModelContext, ModelHandle,
|
Entity, ModelContext, ModelHandle,
|
||||||
};
|
};
|
||||||
use inlay_map::InlayMap;
|
use inlay_map::InlayMap;
|
||||||
use language::{
|
use language::{
|
||||||
language_settings::language_settings, OffsetUtf16, Point, Subscription as BufferSubscription,
|
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 sum_tree::{Bias, TreeMap};
|
||||||
use tab_map::TabMap;
|
use tab_map::TabMap;
|
||||||
use wrap_map::WrapMap;
|
use wrap_map::WrapMap;
|
||||||
@ -316,6 +318,12 @@ pub struct Highlights<'a> {
|
|||||||
pub suggestion_highlight_style: Option<HighlightStyle>,
|
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 struct DisplaySnapshot {
|
||||||
pub buffer_snapshot: MultiBufferSnapshot,
|
pub buffer_snapshot: MultiBufferSnapshot,
|
||||||
pub fold_snapshot: fold_map::FoldSnapshot,
|
pub fold_snapshot: fold_map::FoldSnapshot,
|
||||||
@ -485,7 +493,7 @@ impl DisplaySnapshot {
|
|||||||
language_aware: bool,
|
language_aware: bool,
|
||||||
inlay_highlight_style: Option<HighlightStyle>,
|
inlay_highlight_style: Option<HighlightStyle>,
|
||||||
suggestion_highlight_style: Option<HighlightStyle>,
|
suggestion_highlight_style: Option<HighlightStyle>,
|
||||||
) -> DisplayChunks<'_> {
|
) -> DisplayChunks<'a> {
|
||||||
self.block_snapshot.chunks(
|
self.block_snapshot.chunks(
|
||||||
display_rows,
|
display_rows,
|
||||||
language_aware,
|
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(
|
pub fn chars_at(
|
||||||
&self,
|
&self,
|
||||||
mut point: DisplayPoint,
|
mut point: DisplayPoint,
|
||||||
@ -869,12 +1011,16 @@ pub fn next_rows(display_row: u32, display_map: &DisplaySnapshot) -> impl Iterat
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub mod tests {
|
pub mod tests {
|
||||||
use super::*;
|
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 gpui::{color::Color, elements::*, test::observe, AppContext};
|
||||||
use language::{
|
use language::{
|
||||||
language_settings::{AllLanguageSettings, AllLanguageSettingsContent},
|
language_settings::{AllLanguageSettings, AllLanguageSettingsContent},
|
||||||
Buffer, Language, LanguageConfig, SelectionGoal,
|
Buffer, Language, LanguageConfig, SelectionGoal,
|
||||||
};
|
};
|
||||||
|
use project::Project;
|
||||||
use rand::{prelude::*, Rng};
|
use rand::{prelude::*, Rng};
|
||||||
use settings::SettingsStore;
|
use settings::SettingsStore;
|
||||||
use smol::stream::StreamExt;
|
use smol::stream::StreamExt;
|
||||||
@ -1148,95 +1294,120 @@ pub mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test(retries = 5)]
|
#[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);
|
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 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 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));
|
let mut cx = EditorTestContext::new(cx).await;
|
||||||
assert_eq!(
|
let editor = cx.editor.clone();
|
||||||
snapshot.text_chunks(0).collect::<String>(),
|
let window = cx.window.clone();
|
||||||
"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 ix = snapshot.buffer_snapshot.text().find("seven").unwrap();
|
cx.update_window(window, |cx| {
|
||||||
buffer.update(cx, |buffer, cx| {
|
let text_layout_details =
|
||||||
buffer.edit([(ix..ix, "and ")], None, cx);
|
editor.read_with(cx, |editor, cx| editor.text_layout_details(cx));
|
||||||
|
|
||||||
|
let font_cache = cx.font_cache().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.);
|
||||||
|
|
||||||
|
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(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)
|
||||||
|
);
|
||||||
|
|
||||||
|
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 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"
|
||||||
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
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]
|
#[gpui::test]
|
||||||
@ -1731,6 +1902,9 @@ pub mod tests {
|
|||||||
cx.foreground().forbid_parking();
|
cx.foreground().forbid_parking();
|
||||||
cx.set_global(SettingsStore::test(cx));
|
cx.set_global(SettingsStore::test(cx));
|
||||||
language::init(cx);
|
language::init(cx);
|
||||||
|
crate::init(cx);
|
||||||
|
Project::init_settings(cx);
|
||||||
|
theme::init((), cx);
|
||||||
cx.update_global::<SettingsStore, _, _>(|store, cx| {
|
cx.update_global::<SettingsStore, _, _>(|store, cx| {
|
||||||
store.update_user_settings::<AllLanguageSettings>(cx, f);
|
store.update_user_settings::<AllLanguageSettings>(cx, f);
|
||||||
});
|
});
|
||||||
|
@ -71,6 +71,7 @@ use link_go_to_definition::{
|
|||||||
};
|
};
|
||||||
use log::error;
|
use log::error;
|
||||||
use lsp::LanguageServerId;
|
use lsp::LanguageServerId;
|
||||||
|
use movement::TextLayoutDetails;
|
||||||
use multi_buffer::ToOffsetUtf16;
|
use multi_buffer::ToOffsetUtf16;
|
||||||
pub use multi_buffer::{
|
pub use multi_buffer::{
|
||||||
Anchor, AnchorRangeExt, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, ToOffset,
|
Anchor, AnchorRangeExt, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, ToOffset,
|
||||||
@ -3476,6 +3477,14 @@ impl Editor {
|
|||||||
.collect()
|
.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(
|
fn splice_inlay_hints(
|
||||||
&self,
|
&self,
|
||||||
to_remove: Vec<InlayId>,
|
to_remove: Vec<InlayId>,
|
||||||
@ -5410,6 +5419,7 @@ impl Editor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn transpose(&mut self, _: &Transpose, cx: &mut ViewContext<Self>) {
|
pub fn transpose(&mut self, _: &Transpose, cx: &mut ViewContext<Self>) {
|
||||||
|
let text_layout_details = &self.text_layout_details(cx);
|
||||||
self.transact(cx, |this, cx| {
|
self.transact(cx, |this, cx| {
|
||||||
let edits = this.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
let edits = this.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||||
let mut edits: Vec<(Range<usize>, String)> = Default::default();
|
let mut edits: Vec<(Range<usize>, String)> = Default::default();
|
||||||
@ -5433,7 +5443,10 @@ impl Editor {
|
|||||||
|
|
||||||
*head.column_mut() += 1;
|
*head.column_mut() += 1;
|
||||||
head = display_map.clip_point(head, Bias::Right);
|
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
|
let transpose_start = display_map
|
||||||
.buffer_snapshot
|
.buffer_snapshot
|
||||||
@ -5697,13 +5710,21 @@ impl Editor {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let text_layout_details = &self.text_layout_details(cx);
|
||||||
|
|
||||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||||
let line_mode = s.line_mode;
|
let line_mode = s.line_mode;
|
||||||
s.move_with(|map, selection| {
|
s.move_with(|map, selection| {
|
||||||
if !selection.is_empty() && !line_mode {
|
if !selection.is_empty() && !line_mode {
|
||||||
selection.goal = SelectionGoal::None;
|
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);
|
selection.collapse_to(cursor, goal);
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
@ -5731,22 +5752,33 @@ impl Editor {
|
|||||||
Autoscroll::fit()
|
Autoscroll::fit()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let text_layout_details = &self.text_layout_details(cx);
|
||||||
|
|
||||||
self.change_selections(Some(autoscroll), cx, |s| {
|
self.change_selections(Some(autoscroll), cx, |s| {
|
||||||
let line_mode = s.line_mode;
|
let line_mode = s.line_mode;
|
||||||
s.move_with(|map, selection| {
|
s.move_with(|map, selection| {
|
||||||
if !selection.is_empty() && !line_mode {
|
if !selection.is_empty() && !line_mode {
|
||||||
selection.goal = SelectionGoal::None;
|
selection.goal = SelectionGoal::None;
|
||||||
}
|
}
|
||||||
let (cursor, goal) =
|
let (cursor, goal) = movement::up_by_rows(
|
||||||
movement::up_by_rows(map, selection.end, row_count, selection.goal, false);
|
map,
|
||||||
|
selection.end,
|
||||||
|
row_count,
|
||||||
|
selection.goal,
|
||||||
|
false,
|
||||||
|
&text_layout_details,
|
||||||
|
);
|
||||||
selection.collapse_to(cursor, goal);
|
selection.collapse_to(cursor, goal);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn select_up(&mut self, _: &SelectUp, cx: &mut ViewContext<Self>) {
|
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| {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let text_layout_details = &self.text_layout_details(cx);
|
||||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||||
let line_mode = s.line_mode;
|
let line_mode = s.line_mode;
|
||||||
s.move_with(|map, selection| {
|
s.move_with(|map, selection| {
|
||||||
if !selection.is_empty() && !line_mode {
|
if !selection.is_empty() && !line_mode {
|
||||||
selection.goal = SelectionGoal::None;
|
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);
|
selection.collapse_to(cursor, goal);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -5802,22 +5841,32 @@ impl Editor {
|
|||||||
Autoscroll::fit()
|
Autoscroll::fit()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let text_layout_details = &self.text_layout_details(cx);
|
||||||
self.change_selections(Some(autoscroll), cx, |s| {
|
self.change_selections(Some(autoscroll), cx, |s| {
|
||||||
let line_mode = s.line_mode;
|
let line_mode = s.line_mode;
|
||||||
s.move_with(|map, selection| {
|
s.move_with(|map, selection| {
|
||||||
if !selection.is_empty() && !line_mode {
|
if !selection.is_empty() && !line_mode {
|
||||||
selection.goal = SelectionGoal::None;
|
selection.goal = SelectionGoal::None;
|
||||||
}
|
}
|
||||||
let (cursor, goal) =
|
let (cursor, goal) = movement::down_by_rows(
|
||||||
movement::down_by_rows(map, selection.end, row_count, selection.goal, false);
|
map,
|
||||||
|
selection.end,
|
||||||
|
row_count,
|
||||||
|
selection.goal,
|
||||||
|
false,
|
||||||
|
&text_layout_details,
|
||||||
|
);
|
||||||
selection.collapse_to(cursor, goal);
|
selection.collapse_to(cursor, goal);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn select_down(&mut self, _: &SelectDown, cx: &mut ViewContext<Self>) {
|
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| {
|
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>) {
|
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 display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
|
||||||
let mut selections = self.selections.all::<Point>(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 mut state = self.add_selections_state.take().unwrap_or_else(|| {
|
||||||
let oldest_selection = selections.iter().min_by_key(|s| s.id).unwrap().clone();
|
let oldest_selection = selections.iter().min_by_key(|s| s.id).unwrap().clone();
|
||||||
let range = oldest_selection.display_range(&display_map).sorted();
|
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();
|
selections.clear();
|
||||||
let mut stack = Vec::new();
|
let mut stack = Vec::new();
|
||||||
@ -6348,8 +6400,9 @@ impl Editor {
|
|||||||
if let Some(selection) = self.selections.build_columnar_selection(
|
if let Some(selection) = self.selections.build_columnar_selection(
|
||||||
&display_map,
|
&display_map,
|
||||||
row,
|
row,
|
||||||
&columns,
|
&positions,
|
||||||
oldest_selection.reversed,
|
oldest_selection.reversed,
|
||||||
|
&text_layout_details,
|
||||||
) {
|
) {
|
||||||
stack.push(selection.id);
|
stack.push(selection.id);
|
||||||
selections.push(selection);
|
selections.push(selection);
|
||||||
@ -6377,12 +6430,15 @@ impl Editor {
|
|||||||
let range = selection.display_range(&display_map).sorted();
|
let range = selection.display_range(&display_map).sorted();
|
||||||
debug_assert_eq!(range.start.row(), range.end.row());
|
debug_assert_eq!(range.start.row(), range.end.row());
|
||||||
let mut row = range.start.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
|
start..end
|
||||||
} else {
|
} else {
|
||||||
cmp::min(range.start.column(), range.end.column())
|
let start_x = display_map.x_for_point(range.start, &text_layout_details);
|
||||||
..cmp::max(range.start.column(), range.end.column())
|
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 {
|
while row != end_row {
|
||||||
@ -6395,8 +6451,9 @@ impl Editor {
|
|||||||
if let Some(new_selection) = self.selections.build_columnar_selection(
|
if let Some(new_selection) = self.selections.build_columnar_selection(
|
||||||
&display_map,
|
&display_map,
|
||||||
row,
|
row,
|
||||||
&columns,
|
&positions,
|
||||||
selection.reversed,
|
selection.reversed,
|
||||||
|
&text_layout_details,
|
||||||
) {
|
) {
|
||||||
state.stack.push(new_selection.id);
|
state.stack.push(new_selection.id);
|
||||||
if above {
|
if above {
|
||||||
@ -6690,6 +6747,7 @@ impl Editor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn toggle_comments(&mut self, action: &ToggleComments, cx: &mut ViewContext<Self>) {
|
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| {
|
self.transact(cx, |this, cx| {
|
||||||
let mut selections = this.selections.all::<Point>(cx);
|
let mut selections = this.selections.all::<Point>(cx);
|
||||||
let mut edits = Vec::new();
|
let mut edits = Vec::new();
|
||||||
@ -6932,7 +6990,10 @@ impl Editor {
|
|||||||
point.row += 1;
|
point.row += 1;
|
||||||
point = snapshot.clip_point(point, Bias::Left);
|
point = snapshot.clip_point(point, Bias::Left);
|
||||||
let display_point = point.to_display_point(display_snapshot);
|
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
|
let view = cx
|
||||||
.add_window(|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)
|
build_editor(buffer.clone(), cx)
|
||||||
})
|
})
|
||||||
.root(cx);
|
.root(cx);
|
||||||
@ -869,7 +869,7 @@ fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
|
|||||||
true,
|
true,
|
||||||
cx,
|
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);
|
view.move_right(&MoveRight, cx);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@ -888,6 +888,11 @@ fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
view.move_down(&MoveDown, cx);
|
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!(
|
assert_eq!(
|
||||||
view.selections.display_ranges(cx),
|
view.selections.display_ranges(cx),
|
||||||
&[empty_range(1, "ab⋯".len())]
|
&[empty_range(1, "ab⋯".len())]
|
||||||
@ -929,17 +934,18 @@ fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
|
|||||||
view.selections.display_ranges(cx),
|
view.selections.display_ranges(cx),
|
||||||
&[empty_range(1, "ab⋯e".len())]
|
&[empty_range(1, "ab⋯e".len())]
|
||||||
);
|
);
|
||||||
|
view.move_down(&MoveDown, cx);
|
||||||
|
assert_eq!(
|
||||||
|
view.selections.display_ranges(cx),
|
||||||
|
&[empty_range(2, "αβ⋯ε".len())]
|
||||||
|
);
|
||||||
view.move_up(&MoveUp, cx);
|
view.move_up(&MoveUp, cx);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
view.selections.display_ranges(cx),
|
view.selections.display_ranges(cx),
|
||||||
&[empty_range(0, "ⓐⓑ⋯ⓔ".len())]
|
&[empty_range(1, "ab⋯e".len())]
|
||||||
);
|
);
|
||||||
view.move_left(&MoveLeft, cx);
|
|
||||||
assert_eq!(
|
view.move_up(&MoveUp, cx);
|
||||||
view.selections.display_ranges(cx),
|
|
||||||
&[empty_range(0, "ⓐⓑ⋯".len())]
|
|
||||||
);
|
|
||||||
view.move_left(&MoveLeft, cx);
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
view.selections.display_ranges(cx),
|
view.selections.display_ranges(cx),
|
||||||
&[empty_range(0, "ⓐⓑ".len())]
|
&[empty_range(0, "ⓐⓑ".len())]
|
||||||
@ -949,6 +955,11 @@ fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
|
|||||||
view.selections.display_ranges(cx),
|
view.selections.display_ranges(cx),
|
||||||
&[empty_range(0, "ⓐ".len())]
|
&[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,
|
MAX_LINE_LEN,
|
||||||
};
|
};
|
||||||
use crate::{
|
use crate::{
|
||||||
display_map::{BlockStyle, DisplaySnapshot, FoldStatus, TransformBlock},
|
display_map::{BlockStyle, DisplaySnapshot, FoldStatus, HighlightedChunk, TransformBlock},
|
||||||
editor_settings::ShowScrollbar,
|
editor_settings::ShowScrollbar,
|
||||||
git::{diff_hunk_to_display, DisplayDiffHunk},
|
git::{diff_hunk_to_display, DisplayDiffHunk},
|
||||||
hover_popover::{
|
hover_popover::{
|
||||||
@ -22,7 +22,7 @@ use git::diff::DiffHunkStatus;
|
|||||||
use gpui::{
|
use gpui::{
|
||||||
color::Color,
|
color::Color,
|
||||||
elements::*,
|
elements::*,
|
||||||
fonts::{HighlightStyle, TextStyle, Underline},
|
fonts::TextStyle,
|
||||||
geometry::{
|
geometry::{
|
||||||
rect::RectF,
|
rect::RectF,
|
||||||
vector::{vec2f, Vector2F},
|
vector::{vec2f, Vector2F},
|
||||||
@ -37,8 +37,7 @@ use gpui::{
|
|||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use json::json;
|
use json::json;
|
||||||
use language::{
|
use language::{
|
||||||
language_settings::ShowWhitespaceSetting, Bias, CursorShape, DiagnosticSeverity, OffsetUtf16,
|
language_settings::ShowWhitespaceSetting, Bias, CursorShape, OffsetUtf16, Selection,
|
||||||
Selection,
|
|
||||||
};
|
};
|
||||||
use project::{
|
use project::{
|
||||||
project_settings::{GitGutterSetting, ProjectSettings},
|
project_settings::{GitGutterSetting, ProjectSettings},
|
||||||
@ -1584,56 +1583,7 @@ impl EditorElement {
|
|||||||
.collect()
|
.collect()
|
||||||
} else {
|
} else {
|
||||||
let style = &self.style;
|
let style = &self.style;
|
||||||
let chunks = snapshot
|
let chunks = snapshot.highlighted_chunks(rows.clone(), true, style);
|
||||||
.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,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
LineWithInvisibles::from_chunks(
|
LineWithInvisibles::from_chunks(
|
||||||
chunks,
|
chunks,
|
||||||
@ -1870,12 +1820,6 @@ impl EditorElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct HighlightedChunk<'a> {
|
|
||||||
chunk: &'a str,
|
|
||||||
style: Option<HighlightStyle>,
|
|
||||||
is_tab: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct LineWithInvisibles {
|
pub struct LineWithInvisibles {
|
||||||
pub line: Line,
|
pub line: Line,
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
use super::{Bias, DisplayPoint, DisplaySnapshot, SelectionGoal, ToDisplayPoint};
|
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 language::Point;
|
||||||
use std::ops::Range;
|
use std::{ops::Range, sync::Arc};
|
||||||
|
|
||||||
#[derive(Debug, PartialEq)]
|
#[derive(Debug, PartialEq)]
|
||||||
pub enum FindRange {
|
pub enum FindRange {
|
||||||
@ -9,6 +10,14 @@ pub enum FindRange {
|
|||||||
MultiLine,
|
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 {
|
pub fn left(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
|
||||||
if point.column() > 0 {
|
if point.column() > 0 {
|
||||||
*point.column_mut() -= 1;
|
*point.column_mut() -= 1;
|
||||||
@ -47,8 +56,16 @@ pub fn up(
|
|||||||
start: DisplayPoint,
|
start: DisplayPoint,
|
||||||
goal: SelectionGoal,
|
goal: SelectionGoal,
|
||||||
preserve_column_at_start: bool,
|
preserve_column_at_start: bool,
|
||||||
|
text_layout_details: &TextLayoutDetails,
|
||||||
) -> (DisplayPoint, SelectionGoal) {
|
) -> (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(
|
pub fn down(
|
||||||
@ -56,8 +73,16 @@ pub fn down(
|
|||||||
start: DisplayPoint,
|
start: DisplayPoint,
|
||||||
goal: SelectionGoal,
|
goal: SelectionGoal,
|
||||||
preserve_column_at_end: bool,
|
preserve_column_at_end: bool,
|
||||||
|
text_layout_details: &TextLayoutDetails,
|
||||||
) -> (DisplayPoint, SelectionGoal) {
|
) -> (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(
|
pub fn up_by_rows(
|
||||||
@ -66,11 +91,13 @@ pub fn up_by_rows(
|
|||||||
row_count: u32,
|
row_count: u32,
|
||||||
goal: SelectionGoal,
|
goal: SelectionGoal,
|
||||||
preserve_column_at_start: bool,
|
preserve_column_at_start: bool,
|
||||||
|
text_layout_details: &TextLayoutDetails,
|
||||||
) -> (DisplayPoint, SelectionGoal) {
|
) -> (DisplayPoint, SelectionGoal) {
|
||||||
let mut goal_column = match goal {
|
let mut goal_x = match goal {
|
||||||
SelectionGoal::Column(column) => column,
|
SelectionGoal::HorizontalPosition(x) => x,
|
||||||
SelectionGoal::ColumnRange { end, .. } => end,
|
SelectionGoal::WrappedHorizontalPosition((_, x)) => x,
|
||||||
_ => map.column_to_chars(start.row(), start.column()),
|
SelectionGoal::HorizontalRange { end, .. } => end,
|
||||||
|
_ => map.x_for_point(start, text_layout_details),
|
||||||
};
|
};
|
||||||
|
|
||||||
let prev_row = start.row().saturating_sub(row_count);
|
let prev_row = start.row().saturating_sub(row_count);
|
||||||
@ -79,19 +106,19 @@ pub fn up_by_rows(
|
|||||||
Bias::Left,
|
Bias::Left,
|
||||||
);
|
);
|
||||||
if point.row() < start.row() {
|
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 {
|
} else if preserve_column_at_start {
|
||||||
return (start, goal);
|
return (start, goal);
|
||||||
} else {
|
} else {
|
||||||
point = DisplayPoint::new(0, 0);
|
point = DisplayPoint::new(0, 0);
|
||||||
goal_column = 0;
|
goal_x = 0.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut clipped_point = map.clip_point(point, Bias::Left);
|
let mut clipped_point = map.clip_point(point, Bias::Left);
|
||||||
if clipped_point.row() < point.row() {
|
if clipped_point.row() < point.row() {
|
||||||
clipped_point = map.clip_point(point, Bias::Right);
|
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(
|
pub fn down_by_rows(
|
||||||
@ -100,29 +127,31 @@ pub fn down_by_rows(
|
|||||||
row_count: u32,
|
row_count: u32,
|
||||||
goal: SelectionGoal,
|
goal: SelectionGoal,
|
||||||
preserve_column_at_end: bool,
|
preserve_column_at_end: bool,
|
||||||
|
text_layout_details: &TextLayoutDetails,
|
||||||
) -> (DisplayPoint, SelectionGoal) {
|
) -> (DisplayPoint, SelectionGoal) {
|
||||||
let mut goal_column = match goal {
|
let mut goal_x = match goal {
|
||||||
SelectionGoal::Column(column) => column,
|
SelectionGoal::HorizontalPosition(x) => x,
|
||||||
SelectionGoal::ColumnRange { end, .. } => end,
|
SelectionGoal::WrappedHorizontalPosition((_, x)) => x,
|
||||||
_ => map.column_to_chars(start.row(), start.column()),
|
SelectionGoal::HorizontalRange { end, .. } => end,
|
||||||
|
_ => map.x_for_point(start, text_layout_details),
|
||||||
};
|
};
|
||||||
|
|
||||||
let new_row = start.row() + row_count;
|
let new_row = start.row() + row_count;
|
||||||
let mut point = map.clip_point(DisplayPoint::new(new_row, 0), Bias::Right);
|
let mut point = map.clip_point(DisplayPoint::new(new_row, 0), Bias::Right);
|
||||||
if point.row() > start.row() {
|
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 {
|
} else if preserve_column_at_end {
|
||||||
return (start, goal);
|
return (start, goal);
|
||||||
} else {
|
} else {
|
||||||
point = map.max_point();
|
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);
|
let mut clipped_point = map.clip_point(point, Bias::Right);
|
||||||
if clipped_point.row() > point.row() {
|
if clipped_point.row() > point.row() {
|
||||||
clipped_point = map.clip_point(point, Bias::Left);
|
clipped_point = map.clip_point(point, Bias::Left);
|
||||||
}
|
}
|
||||||
(clipped_point, SelectionGoal::Column(goal_column))
|
(clipped_point, SelectionGoal::HorizontalPosition(goal_x))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn line_beginning(
|
pub fn line_beginning(
|
||||||
@ -396,9 +425,11 @@ pub fn split_display_range_by_lines(
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::{
|
use crate::{
|
||||||
display_map::Inlay, test::marked_display_snapshot, Buffer, DisplayMap, ExcerptRange,
|
display_map::Inlay,
|
||||||
InlayId, MultiBuffer,
|
test::{editor_test_context::EditorTestContext, marked_display_snapshot},
|
||||||
|
Buffer, DisplayMap, ExcerptRange, InlayId, MultiBuffer,
|
||||||
};
|
};
|
||||||
|
use project::Project;
|
||||||
use settings::SettingsStore;
|
use settings::SettingsStore;
|
||||||
use util::post_inc;
|
use util::post_inc;
|
||||||
|
|
||||||
@ -691,123 +722,173 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
fn test_move_up_and_down_with_excerpts(cx: &mut gpui::AppContext) {
|
async fn test_move_up_and_down_with_excerpts(cx: &mut gpui::TestAppContext) {
|
||||||
init_test(cx);
|
cx.update(|cx| {
|
||||||
|
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();
|
|
||||||
|
|
||||||
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 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));
|
||||||
|
|
||||||
// Can't move up into the first excerpt's header
|
let family_id = cx
|
||||||
assert_eq!(
|
.font_cache()
|
||||||
up(
|
.load_family(&["Helvetica"], &Default::default())
|
||||||
&snapshot,
|
.unwrap();
|
||||||
DisplayPoint::new(2, 2),
|
let font_id = cx
|
||||||
SelectionGoal::Column(2),
|
.font_cache()
|
||||||
false
|
.select_font(family_id, &Default::default())
|
||||||
),
|
.unwrap();
|
||||||
(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
|
let buffer =
|
||||||
assert_eq!(
|
cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "abc\ndefg\nhijkl\nmn"));
|
||||||
up(
|
let multibuffer = cx.add_model(|cx| {
|
||||||
&snapshot,
|
let mut multibuffer = MultiBuffer::new(0);
|
||||||
DisplayPoint::new(3, 4),
|
multibuffer.push_excerpts(
|
||||||
SelectionGoal::Column(4),
|
buffer.clone(),
|
||||||
false
|
[
|
||||||
),
|
ExcerptRange {
|
||||||
(DisplayPoint::new(2, 3), SelectionGoal::Column(4)),
|
context: Point::new(0, 0)..Point::new(1, 4),
|
||||||
);
|
primary: None,
|
||||||
assert_eq!(
|
},
|
||||||
down(
|
ExcerptRange {
|
||||||
&snapshot,
|
context: Point::new(2, 0)..Point::new(3, 2),
|
||||||
DisplayPoint::new(2, 3),
|
primary: None,
|
||||||
SelectionGoal::Column(4),
|
},
|
||||||
false
|
],
|
||||||
),
|
cx,
|
||||||
(DisplayPoint::new(3, 4), SelectionGoal::Column(4)),
|
);
|
||||||
);
|
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));
|
||||||
|
|
||||||
// Move up and down across second excerpt's header
|
assert_eq!(snapshot.text(), "\n\nabc\ndefg\n\n\nhijkl\nmn");
|
||||||
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)),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Can't move down off the end
|
let col_2_x = snapshot.x_for_point(DisplayPoint::new(2, 2), &text_layout_details);
|
||||||
assert_eq!(
|
|
||||||
down(
|
// Can't move up into the first excerpt's header
|
||||||
&snapshot,
|
assert_eq!(
|
||||||
DisplayPoint::new(7, 0),
|
up(
|
||||||
SelectionGoal::Column(0),
|
&snapshot,
|
||||||
false
|
DisplayPoint::new(2, 2),
|
||||||
),
|
SelectionGoal::HorizontalPosition(col_2_x),
|
||||||
(DisplayPoint::new(7, 2), SelectionGoal::Column(2)),
|
false,
|
||||||
);
|
&text_layout_details
|
||||||
assert_eq!(
|
),
|
||||||
down(
|
(
|
||||||
&snapshot,
|
DisplayPoint::new(2, 0),
|
||||||
DisplayPoint::new(7, 2),
|
SelectionGoal::HorizontalPosition(0.0)
|
||||||
SelectionGoal::Column(2),
|
),
|
||||||
false
|
);
|
||||||
),
|
assert_eq!(
|
||||||
(DisplayPoint::new(7, 2), SelectionGoal::Column(2)),
|
up(
|
||||||
);
|
&snapshot,
|
||||||
|
DisplayPoint::new(2, 0),
|
||||||
|
SelectionGoal::None,
|
||||||
|
false,
|
||||||
|
&text_layout_details
|
||||||
|
),
|
||||||
|
(
|
||||||
|
DisplayPoint::new(2, 0),
|
||||||
|
SelectionGoal::HorizontalPosition(0.0)
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
let col_4_x = snapshot.x_for_point(DisplayPoint::new(3, 4), &text_layout_details);
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
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::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)
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
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) {
|
fn init_test(cx: &mut gpui::AppContext) {
|
||||||
@ -815,5 +896,6 @@ mod tests {
|
|||||||
theme::init((), cx);
|
theme::init((), cx);
|
||||||
language::init(cx);
|
language::init(cx);
|
||||||
crate::init(cx);
|
crate::init(cx);
|
||||||
|
Project::init_settings(cx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
use std::{
|
use std::{
|
||||||
cell::Ref,
|
cell::Ref,
|
||||||
cmp, iter, mem,
|
iter, mem,
|
||||||
ops::{Deref, DerefMut, Range, Sub},
|
ops::{Deref, DerefMut, Range, Sub},
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
};
|
};
|
||||||
@ -13,6 +13,7 @@ use util::post_inc;
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
display_map::{DisplayMap, DisplaySnapshot, ToDisplayPoint},
|
display_map::{DisplayMap, DisplaySnapshot, ToDisplayPoint},
|
||||||
|
movement::TextLayoutDetails,
|
||||||
Anchor, DisplayPoint, ExcerptId, MultiBuffer, MultiBufferSnapshot, SelectMode, ToOffset,
|
Anchor, DisplayPoint, ExcerptId, MultiBuffer, MultiBufferSnapshot, SelectMode, ToOffset,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -305,23 +306,29 @@ impl SelectionsCollection {
|
|||||||
&mut self,
|
&mut self,
|
||||||
display_map: &DisplaySnapshot,
|
display_map: &DisplaySnapshot,
|
||||||
row: u32,
|
row: u32,
|
||||||
columns: &Range<u32>,
|
positions: &Range<f32>,
|
||||||
reversed: bool,
|
reversed: bool,
|
||||||
|
text_layout_details: &TextLayoutDetails,
|
||||||
) -> Option<Selection<Point>> {
|
) -> 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);
|
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 layed_out_line = display_map.lay_out_line_for_row(row, &text_layout_details);
|
||||||
let end = DisplayPoint::new(row, cmp::min(columns.end, line_len));
|
|
||||||
|
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 {
|
Some(Selection {
|
||||||
id: post_inc(&mut self.next_selection_id),
|
id: post_inc(&mut self.next_selection_id),
|
||||||
start: start.to_point(display_map),
|
start: start.to_point(display_map),
|
||||||
end: end.to_point(display_map),
|
end: end.to_point(display_map),
|
||||||
reversed,
|
reversed,
|
||||||
goal: SelectionGoal::ColumnRange {
|
goal: SelectionGoal::HorizontalRange {
|
||||||
start: columns.start,
|
start: positions.start,
|
||||||
end: columns.end,
|
end: positions.end,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
@ -266,6 +266,8 @@ impl Line {
|
|||||||
self.layout.len == 0
|
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> {
|
pub fn index_for_x(&self, x: f32) -> Option<usize> {
|
||||||
if x >= self.layout.width {
|
if x >= self.layout.width {
|
||||||
None
|
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(
|
pub fn paint(
|
||||||
&self,
|
&self,
|
||||||
origin: Vector2F,
|
origin: Vector2F,
|
||||||
|
@ -201,7 +201,7 @@ pub struct CodeAction {
|
|||||||
pub lsp_action: lsp::CodeAction,
|
pub lsp_action: lsp::CodeAction,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
pub enum Operation {
|
pub enum Operation {
|
||||||
Buffer(text::Operation),
|
Buffer(text::Operation),
|
||||||
|
|
||||||
@ -224,7 +224,7 @@ pub enum Operation {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
pub enum Event {
|
pub enum Event {
|
||||||
Operation(Operation),
|
Operation(Operation),
|
||||||
Edited,
|
Edited,
|
||||||
|
@ -2,14 +2,15 @@ use crate::{Anchor, BufferSnapshot, TextDimension};
|
|||||||
use std::cmp::Ordering;
|
use std::cmp::Ordering;
|
||||||
use std::ops::Range;
|
use std::ops::Range;
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||||
pub enum SelectionGoal {
|
pub enum SelectionGoal {
|
||||||
None,
|
None,
|
||||||
Column(u32),
|
HorizontalPosition(f32),
|
||||||
ColumnRange { start: u32, end: u32 },
|
HorizontalRange { start: f32, end: f32 },
|
||||||
|
WrappedHorizontalPosition((u32, f32)),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
pub struct Selection<T> {
|
pub struct Selection<T> {
|
||||||
pub id: usize,
|
pub id: usize,
|
||||||
pub start: T,
|
pub start: T,
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
use std::cmp;
|
|
||||||
|
|
||||||
use editor::{
|
use editor::{
|
||||||
char_kind,
|
char_kind,
|
||||||
display_map::{DisplaySnapshot, FoldPoint, ToDisplayPoint},
|
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,
|
Bias, CharKind, DisplayPoint, ToOffset,
|
||||||
};
|
};
|
||||||
use gpui::{actions, impl_actions, AppContext, WindowContext};
|
use gpui::{actions, impl_actions, AppContext, WindowContext};
|
||||||
@ -361,6 +359,7 @@ impl Motion {
|
|||||||
point: DisplayPoint,
|
point: DisplayPoint,
|
||||||
goal: SelectionGoal,
|
goal: SelectionGoal,
|
||||||
maybe_times: Option<usize>,
|
maybe_times: Option<usize>,
|
||||||
|
text_layout_details: &TextLayoutDetails,
|
||||||
) -> Option<(DisplayPoint, SelectionGoal)> {
|
) -> Option<(DisplayPoint, SelectionGoal)> {
|
||||||
let times = maybe_times.unwrap_or(1);
|
let times = maybe_times.unwrap_or(1);
|
||||||
use Motion::*;
|
use Motion::*;
|
||||||
@ -370,16 +369,16 @@ impl Motion {
|
|||||||
Backspace => (backspace(map, point, times), SelectionGoal::None),
|
Backspace => (backspace(map, point, times), SelectionGoal::None),
|
||||||
Down {
|
Down {
|
||||||
display_lines: false,
|
display_lines: false,
|
||||||
} => down(map, point, goal, times),
|
} => up_down_buffer_rows(map, point, goal, times as isize, &text_layout_details),
|
||||||
Down {
|
Down {
|
||||||
display_lines: true,
|
display_lines: true,
|
||||||
} => down_display(map, point, goal, times),
|
} => down_display(map, point, goal, times, &text_layout_details),
|
||||||
Up {
|
Up {
|
||||||
display_lines: false,
|
display_lines: false,
|
||||||
} => up(map, point, goal, times),
|
} => up_down_buffer_rows(map, point, goal, 0 - times as isize, &text_layout_details),
|
||||||
Up {
|
Up {
|
||||||
display_lines: true,
|
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),
|
Right => (right(map, point, times), SelectionGoal::None),
|
||||||
NextWordStart { ignore_punctuation } => (
|
NextWordStart { ignore_punctuation } => (
|
||||||
next_word_start(map, point, *ignore_punctuation, times),
|
next_word_start(map, point, *ignore_punctuation, times),
|
||||||
@ -442,10 +441,15 @@ impl Motion {
|
|||||||
selection: &mut Selection<DisplayPoint>,
|
selection: &mut Selection<DisplayPoint>,
|
||||||
times: Option<usize>,
|
times: Option<usize>,
|
||||||
expand_to_surrounding_newline: bool,
|
expand_to_surrounding_newline: bool,
|
||||||
|
text_layout_details: &TextLayoutDetails,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
if let Some((new_head, goal)) =
|
if let Some((new_head, goal)) = self.move_point(
|
||||||
self.move_point(map, selection.head(), selection.goal, times)
|
map,
|
||||||
{
|
selection.head(),
|
||||||
|
selection.goal,
|
||||||
|
times,
|
||||||
|
&text_layout_details,
|
||||||
|
) {
|
||||||
selection.set_head(new_head, goal);
|
selection.set_head(new_head, goal);
|
||||||
|
|
||||||
if self.linewise() {
|
if self.linewise() {
|
||||||
@ -530,35 +534,85 @@ fn backspace(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> Di
|
|||||||
point
|
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,
|
map: &DisplaySnapshot,
|
||||||
point: DisplayPoint,
|
point: DisplayPoint,
|
||||||
mut goal: SelectionGoal,
|
mut goal: SelectionGoal,
|
||||||
times: usize,
|
times: isize,
|
||||||
|
text_layout_details: &TextLayoutDetails,
|
||||||
) -> (DisplayPoint, SelectionGoal) {
|
) -> (DisplayPoint, SelectionGoal) {
|
||||||
let start = map.display_point_to_fold_point(point, Bias::Left);
|
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 {
|
let (goal_wrap, goal_x) = match goal {
|
||||||
SelectionGoal::Column(column) => column,
|
SelectionGoal::WrappedHorizontalPosition((row, x)) => (row, x),
|
||||||
SelectionGoal::ColumnRange { end, .. } => end,
|
SelectionGoal::HorizontalRange { end, .. } => (select_nth_wrapped_row, end),
|
||||||
|
SelectionGoal::HorizontalPosition(x) => (select_nth_wrapped_row, x),
|
||||||
_ => {
|
_ => {
|
||||||
goal = SelectionGoal::Column(start.column());
|
let x = map.x_for_point(point, text_layout_details);
|
||||||
start.column()
|
goal = SelectionGoal::WrappedHorizontalPosition((select_nth_wrapped_row, x));
|
||||||
|
(select_nth_wrapped_row, x)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let new_row = cmp::min(
|
let target = start.row() as isize + times;
|
||||||
start.row() + times as u32,
|
let new_row = (target.max(0) as u32).min(map.fold_snapshot.max_point().row());
|
||||||
map.fold_snapshot.max_point().row(),
|
|
||||||
);
|
let mut begin_folded_line = map.fold_point_to_display_point(
|
||||||
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
|
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"
|
let mut i = 0;
|
||||||
(map.clip_point(point, Bias::Left), goal)
|
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(
|
fn down_display(
|
||||||
@ -566,49 +620,24 @@ fn down_display(
|
|||||||
mut point: DisplayPoint,
|
mut point: DisplayPoint,
|
||||||
mut goal: SelectionGoal,
|
mut goal: SelectionGoal,
|
||||||
times: usize,
|
times: usize,
|
||||||
|
text_layout_details: &TextLayoutDetails,
|
||||||
) -> (DisplayPoint, SelectionGoal) {
|
) -> (DisplayPoint, SelectionGoal) {
|
||||||
for _ in 0..times {
|
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)
|
(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(
|
fn up_display(
|
||||||
map: &DisplaySnapshot,
|
map: &DisplaySnapshot,
|
||||||
mut point: DisplayPoint,
|
mut point: DisplayPoint,
|
||||||
mut goal: SelectionGoal,
|
mut goal: SelectionGoal,
|
||||||
times: usize,
|
times: usize,
|
||||||
|
text_layout_details: &TextLayoutDetails,
|
||||||
) -> (DisplayPoint, SelectionGoal) {
|
) -> (DisplayPoint, SelectionGoal) {
|
||||||
for _ in 0..times {
|
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)
|
(point, goal)
|
||||||
@ -707,7 +736,7 @@ fn previous_word_start(
|
|||||||
point
|
point
|
||||||
}
|
}
|
||||||
|
|
||||||
fn first_non_whitespace(
|
pub(crate) fn first_non_whitespace(
|
||||||
map: &DisplaySnapshot,
|
map: &DisplaySnapshot,
|
||||||
display_lines: bool,
|
display_lines: bool,
|
||||||
from: DisplayPoint,
|
from: DisplayPoint,
|
||||||
@ -886,13 +915,17 @@ fn find_backward(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
|
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)
|
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 {
|
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)
|
end_of_line(map, false, point)
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,7 @@ mod yank;
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
motion::{self, Motion},
|
motion::{self, first_non_whitespace, next_line_end, right, Motion},
|
||||||
object::Object,
|
object::Object,
|
||||||
state::{Mode, Operator},
|
state::{Mode, Operator},
|
||||||
Vim,
|
Vim,
|
||||||
@ -179,10 +179,11 @@ pub(crate) fn move_cursor(
|
|||||||
cx: &mut WindowContext,
|
cx: &mut WindowContext,
|
||||||
) {
|
) {
|
||||||
vim.update_active_editor(cx, |editor, cx| {
|
vim.update_active_editor(cx, |editor, cx| {
|
||||||
|
let text_layout_details = editor.text_layout_details(cx);
|
||||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||||
s.move_cursors_with(|map, cursor, goal| {
|
s.move_cursors_with(|map, cursor, goal| {
|
||||||
motion
|
motion
|
||||||
.move_point(map, cursor, goal, times)
|
.move_point(map, cursor, goal, times, &text_layout_details)
|
||||||
.unwrap_or((cursor, goal))
|
.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.switch_mode(Mode::Insert, false, cx);
|
||||||
vim.update_active_editor(cx, |editor, cx| {
|
vim.update_active_editor(cx, |editor, cx| {
|
||||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||||
s.maybe_move_cursors_with(|map, cursor, goal| {
|
s.move_cursors_with(|map, cursor, _| (right(map, cursor, 1), SelectionGoal::None));
|
||||||
Motion::Right.move_point(map, cursor, goal, None)
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -220,11 +219,11 @@ fn insert_first_non_whitespace(
|
|||||||
vim.switch_mode(Mode::Insert, false, cx);
|
vim.switch_mode(Mode::Insert, false, cx);
|
||||||
vim.update_active_editor(cx, |editor, cx| {
|
vim.update_active_editor(cx, |editor, cx| {
|
||||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||||
s.maybe_move_cursors_with(|map, cursor, goal| {
|
s.move_cursors_with(|map, cursor, _| {
|
||||||
Motion::FirstNonWhitespace {
|
(
|
||||||
display_lines: false,
|
first_non_whitespace(map, false, cursor),
|
||||||
}
|
SelectionGoal::None,
|
||||||
.move_point(map, cursor, goal, None)
|
)
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -237,8 +236,8 @@ fn insert_end_of_line(_: &mut Workspace, _: &InsertEndOfLine, cx: &mut ViewConte
|
|||||||
vim.switch_mode(Mode::Insert, false, cx);
|
vim.switch_mode(Mode::Insert, false, cx);
|
||||||
vim.update_active_editor(cx, |editor, cx| {
|
vim.update_active_editor(cx, |editor, cx| {
|
||||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||||
s.maybe_move_cursors_with(|map, cursor, goal| {
|
s.move_cursors_with(|map, cursor, _| {
|
||||||
Motion::CurrentLine.move_point(map, cursor, goal, None)
|
(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.edit_with_autoindent(edits, cx);
|
||||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||||
s.move_cursors_with(|map, cursor, _| {
|
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);
|
let insert_point = motion::end_of_line(map, false, previous_line);
|
||||||
(insert_point, SelectionGoal::None)
|
(insert_point, SelectionGoal::None)
|
||||||
});
|
});
|
||||||
@ -283,6 +282,7 @@ fn insert_line_below(_: &mut Workspace, _: &InsertLineBelow, cx: &mut ViewContex
|
|||||||
vim.start_recording(cx);
|
vim.start_recording(cx);
|
||||||
vim.switch_mode(Mode::Insert, false, cx);
|
vim.switch_mode(Mode::Insert, false, cx);
|
||||||
vim.update_active_editor(cx, |editor, cx| {
|
vim.update_active_editor(cx, |editor, cx| {
|
||||||
|
let text_layout_details = editor.text_layout_details(cx);
|
||||||
editor.transact(cx, |editor, cx| {
|
editor.transact(cx, |editor, cx| {
|
||||||
let (map, old_selections) = editor.selections.all_display(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| {
|
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||||
s.maybe_move_cursors_with(|map, cursor, goal| {
|
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);
|
editor.edit_with_autoindent(edits, cx);
|
||||||
@ -399,12 +405,26 @@ mod test {
|
|||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_j(cx: &mut gpui::TestAppContext) {
|
async fn test_j(cx: &mut gpui::TestAppContext) {
|
||||||
let mut cx = NeovimBackedTestContext::new(cx).await.binding(["j"]);
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||||
cx.assert_all(indoc! {"
|
|
||||||
ˇThe qˇuick broˇwn
|
cx.set_shared_state(indoc! {"
|
||||||
ˇfox jumps"
|
aaˇaa
|
||||||
|
😃😃"
|
||||||
})
|
})
|
||||||
.await;
|
.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]
|
#[gpui::test]
|
||||||
|
@ -2,7 +2,7 @@ use crate::{motion::Motion, object::Object, state::Mode, utils::copy_selections_
|
|||||||
use editor::{
|
use editor::{
|
||||||
char_kind,
|
char_kind,
|
||||||
display_map::DisplaySnapshot,
|
display_map::DisplaySnapshot,
|
||||||
movement::{self, FindRange},
|
movement::{self, FindRange, TextLayoutDetails},
|
||||||
scroll::autoscroll::Autoscroll,
|
scroll::autoscroll::Autoscroll,
|
||||||
CharKind, DisplayPoint,
|
CharKind, DisplayPoint,
|
||||||
};
|
};
|
||||||
@ -20,6 +20,7 @@ pub fn change_motion(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &m
|
|||||||
| Motion::StartOfLine { .. }
|
| Motion::StartOfLine { .. }
|
||||||
);
|
);
|
||||||
vim.update_active_editor(cx, |editor, cx| {
|
vim.update_active_editor(cx, |editor, cx| {
|
||||||
|
let text_layout_details = editor.text_layout_details(cx);
|
||||||
editor.transact(cx, |editor, cx| {
|
editor.transact(cx, |editor, cx| {
|
||||||
// We are swapping to insert mode anyway. Just set the line end clipping behavior now
|
// We are swapping to insert mode anyway. Just set the line end clipping behavior now
|
||||||
editor.set_clip_at_line_ends(false, cx);
|
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| {
|
s.move_with(|map, selection| {
|
||||||
motion_succeeded |= if let Motion::NextWordStart { ignore_punctuation } = motion
|
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 {
|
} 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>,
|
selection: &mut Selection<DisplayPoint>,
|
||||||
times: Option<usize>,
|
times: Option<usize>,
|
||||||
ignore_punctuation: bool,
|
ignore_punctuation: bool,
|
||||||
|
text_layout_details: &TextLayoutDetails,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
if times.is_none() || times.unwrap() == 1 {
|
if times.is_none() || times.unwrap() == 1 {
|
||||||
let scope = map
|
let scope = map
|
||||||
@ -103,11 +111,22 @@ fn expand_changed_word_selection(
|
|||||||
});
|
});
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
Motion::NextWordStart { ignore_punctuation }
|
Motion::NextWordStart { ignore_punctuation }.expand_selection(
|
||||||
.expand_selection(map, selection, None, false)
|
map,
|
||||||
|
selection,
|
||||||
|
None,
|
||||||
|
false,
|
||||||
|
&text_layout_details,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} 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) {
|
pub fn delete_motion(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &mut WindowContext) {
|
||||||
vim.stop_recording();
|
vim.stop_recording();
|
||||||
vim.update_active_editor(cx, |editor, cx| {
|
vim.update_active_editor(cx, |editor, cx| {
|
||||||
|
let text_layout_details = editor.text_layout_details(cx);
|
||||||
editor.transact(cx, |editor, cx| {
|
editor.transact(cx, |editor, cx| {
|
||||||
editor.set_clip_at_line_ends(false, cx);
|
editor.set_clip_at_line_ends(false, cx);
|
||||||
let mut original_columns: HashMap<_, _> = Default::default();
|
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| {
|
s.move_with(|map, selection| {
|
||||||
let original_head = selection.head();
|
let original_head = selection.head();
|
||||||
original_columns.insert(selection.id, original_head.column());
|
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.
|
// Motion::NextWordStart on an empty line should delete it.
|
||||||
if let Motion::NextWordStart {
|
if let Motion::NextWordStart {
|
||||||
|
@ -255,8 +255,18 @@ mod test {
|
|||||||
4
|
4
|
||||||
5"})
|
5"})
|
||||||
.await;
|
.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;
|
.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! {"
|
cx.assert_shared_state(indoc! {"
|
||||||
ˇ0
|
ˇ0
|
||||||
0
|
0
|
||||||
|
@ -30,6 +30,7 @@ fn paste(_: &mut Workspace, action: &Paste, cx: &mut ViewContext<Workspace>) {
|
|||||||
Vim::update(cx, |vim, cx| {
|
Vim::update(cx, |vim, cx| {
|
||||||
vim.record_current_action(cx);
|
vim.record_current_action(cx);
|
||||||
vim.update_active_editor(cx, |editor, cx| {
|
vim.update_active_editor(cx, |editor, cx| {
|
||||||
|
let text_layout_details = editor.text_layout_details(cx);
|
||||||
editor.transact(cx, |editor, cx| {
|
editor.transact(cx, |editor, cx| {
|
||||||
editor.set_clip_at_line_ends(false, 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);
|
let mut cursor = anchor.to_display_point(map);
|
||||||
if *line_mode {
|
if *line_mode {
|
||||||
if !before {
|
if !before {
|
||||||
cursor =
|
cursor = movement::down(
|
||||||
movement::down(map, cursor, SelectionGoal::None, false).0;
|
map,
|
||||||
|
cursor,
|
||||||
|
SelectionGoal::None,
|
||||||
|
false,
|
||||||
|
&text_layout_details,
|
||||||
|
)
|
||||||
|
.0;
|
||||||
}
|
}
|
||||||
cursor = movement::indented_line_beginning(map, cursor, true);
|
cursor = movement::indented_line_beginning(map, cursor, true);
|
||||||
} else if !is_multiline {
|
} 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| {
|
vim.update_active_editor(cx, |editor, cx| {
|
||||||
editor.set_clip_at_line_ends(false, cx);
|
editor.set_clip_at_line_ends(false, cx);
|
||||||
editor.transact(cx, |editor, cx| {
|
editor.transact(cx, |editor, cx| {
|
||||||
|
let text_layout_details = editor.text_layout_details(cx);
|
||||||
editor.change_selections(None, cx, |s| {
|
editor.change_selections(None, cx, |s| {
|
||||||
s.move_with(|map, selection| {
|
s.move_with(|map, selection| {
|
||||||
if selection.start == selection.end {
|
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 {
|
if line_mode {
|
||||||
// in Visual mode when the selection contains the newline at the end
|
// 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 {
|
if !selection.is_empty() && selection.end.column() == 0 {
|
||||||
selection.end = movement::left(map, selection.end);
|
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 {
|
if let Some((point, _)) = (Motion::FirstNonWhitespace {
|
||||||
display_lines: false,
|
display_lines: false,
|
||||||
})
|
})
|
||||||
@ -52,6 +65,7 @@ pub fn substitute(vim: &mut Vim, count: Option<usize>, line_mode: bool, cx: &mut
|
|||||||
selection.start,
|
selection.start,
|
||||||
selection.goal,
|
selection.goal,
|
||||||
None,
|
None,
|
||||||
|
&text_layout_details,
|
||||||
) {
|
) {
|
||||||
selection.start = point;
|
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) {
|
pub fn yank_motion(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &mut WindowContext) {
|
||||||
vim.update_active_editor(cx, |editor, cx| {
|
vim.update_active_editor(cx, |editor, cx| {
|
||||||
|
let text_layout_details = editor.text_layout_details(cx);
|
||||||
editor.transact(cx, |editor, cx| {
|
editor.transact(cx, |editor, cx| {
|
||||||
editor.set_clip_at_line_ends(false, cx);
|
editor.set_clip_at_line_ends(false, cx);
|
||||||
let mut original_positions: HashMap<_, _> = Default::default();
|
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| {
|
s.move_with(|map, selection| {
|
||||||
let original_position = (selection.head(), selection.goal);
|
let original_position = (selection.head(), selection.goal);
|
||||||
original_positions.insert(selection.id, original_position);
|
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);
|
copy_selections_content(editor, motion.linewise(), cx);
|
||||||
|
@ -653,6 +653,63 @@ async fn test_selection_goal(cx: &mut gpui::TestAppContext) {
|
|||||||
.await;
|
.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]
|
#[gpui::test]
|
||||||
async fn test_paragraphs_dont_wrap(cx: &mut gpui::TestAppContext) {
|
async fn test_paragraphs_dont_wrap(cx: &mut gpui::TestAppContext) {
|
||||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
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) {
|
fn local_selections_changed(newest: Selection<usize>, cx: &mut WindowContext) {
|
||||||
Vim::update(cx, |vim, cx| {
|
Vim::update(cx, |vim, cx| {
|
||||||
if vim.enabled && vim.state().mode == Mode::Normal && !newest.is_empty() {
|
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);
|
vim.switch_mode(Mode::VisualBlock, false, cx);
|
||||||
} else {
|
} else {
|
||||||
vim.switch_mode(Mode::Visual, false, cx)
|
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) {
|
pub fn visual_motion(motion: Motion, times: Option<usize>, cx: &mut WindowContext) {
|
||||||
Vim::update(cx, |vim, cx| {
|
Vim::update(cx, |vim, cx| {
|
||||||
vim.update_active_editor(cx, |editor, cx| {
|
vim.update_active_editor(cx, |editor, cx| {
|
||||||
|
let text_layout_details = editor.text_layout_details(cx);
|
||||||
if vim.state().mode == Mode::VisualBlock
|
if vim.state().mode == Mode::VisualBlock
|
||||||
&& !matches!(
|
&& !matches!(
|
||||||
motion,
|
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 { .. });
|
let is_up_or_down = matches!(motion, Motion::Up { .. } | Motion::Down { .. });
|
||||||
visual_block_motion(is_up_or_down, editor, cx, |map, point, goal| {
|
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 {
|
} else {
|
||||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
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)
|
current_head = movement::left(map, selection.end)
|
||||||
}
|
}
|
||||||
|
|
||||||
let Some((new_head, goal)) =
|
let Some((new_head, goal)) = motion.move_point(
|
||||||
motion.move_point(map, current_head, selection.goal, times)
|
map,
|
||||||
else {
|
current_head,
|
||||||
|
selection.goal,
|
||||||
|
times,
|
||||||
|
&text_layout_details,
|
||||||
|
) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -135,19 +140,23 @@ pub fn visual_block_motion(
|
|||||||
SelectionGoal,
|
SelectionGoal,
|
||||||
) -> Option<(DisplayPoint, SelectionGoal)>,
|
) -> Option<(DisplayPoint, SelectionGoal)>,
|
||||||
) {
|
) {
|
||||||
|
let text_layout_details = editor.text_layout_details(cx);
|
||||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||||
let map = &s.display_map();
|
let map = &s.display_map();
|
||||||
let mut head = s.newest_anchor().head().to_display_point(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 tail = s.oldest_anchor().tail().to_display_point(map);
|
||||||
|
|
||||||
let (start, end) = match s.newest_anchor().goal {
|
let mut head_x = map.x_for_point(head, &text_layout_details);
|
||||||
SelectionGoal::ColumnRange { start, end } if preserve_goal => (start, end),
|
let mut tail_x = map.x_for_point(tail, &text_layout_details);
|
||||||
SelectionGoal::Column(start) if preserve_goal => (start, start + 1),
|
|
||||||
_ => (tail.column(), head.column()),
|
|
||||||
};
|
|
||||||
let goal = SelectionGoal::ColumnRange { start, end };
|
|
||||||
|
|
||||||
let was_reversed = tail.column() > head.column();
|
let (start, end) = match s.newest_anchor().goal {
|
||||||
|
SelectionGoal::HorizontalRange { start, end } if preserve_goal => (start, end),
|
||||||
|
SelectionGoal::HorizontalPosition(start) if preserve_goal => (start, start),
|
||||||
|
_ => (tail_x, head_x),
|
||||||
|
};
|
||||||
|
let mut goal = SelectionGoal::HorizontalRange { start, end };
|
||||||
|
|
||||||
|
let was_reversed = tail_x > head_x;
|
||||||
if !was_reversed && !preserve_goal {
|
if !was_reversed && !preserve_goal {
|
||||||
head = movement::saturating_left(map, head);
|
head = movement::saturating_left(map, head);
|
||||||
}
|
}
|
||||||
@ -156,32 +165,56 @@ pub fn visual_block_motion(
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
head = new_head;
|
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 {
|
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 {
|
} 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 {
|
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 {
|
let positions = if is_reversed {
|
||||||
head.column()..tail.column()
|
head_x..tail_x
|
||||||
} else if head.column() == tail.column() {
|
|
||||||
head.column()..(head.column() + 1)
|
|
||||||
} else {
|
} 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 selections = Vec::new();
|
||||||
let mut row = tail.row();
|
let mut row = tail.row();
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let start = map.clip_point(DisplayPoint::new(row, columns.start), Bias::Left);
|
let layed_out_line = map.lay_out_line_for_row(row, &text_layout_details);
|
||||||
let end = map.clip_point(DisplayPoint::new(row, columns.end), Bias::Left);
|
let start = DisplayPoint::new(
|
||||||
if columns.start <= map.line_len(row) {
|
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 {
|
let selection = Selection {
|
||||||
id: s.new_selection_id(),
|
id: s.new_selection_id(),
|
||||||
start: start.to_point(map),
|
start: start.to_point(map),
|
||||||
@ -888,6 +921,28 @@ mod test {
|
|||||||
.await;
|
.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]
|
#[gpui::test]
|
||||||
async fn test_visual_block_insert(cx: &mut gpui::TestAppContext) {
|
async fn test_visual_block_insert(cx: &mut gpui::TestAppContext) {
|
||||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||||
|
@ -9,6 +9,7 @@
|
|||||||
{"Key":"ctrl-v"}
|
{"Key":"ctrl-v"}
|
||||||
{"Key":"g"}
|
{"Key":"g"}
|
||||||
{"Key":"g"}
|
{"Key":"g"}
|
||||||
|
{"Get":{"state":"«1ˇ»\n«2ˇ»\n«3ˇ» 2\n«4ˇ»\n«5ˇ»","mode":"VisualBlock"}}
|
||||||
{"Key":"g"}
|
{"Key":"g"}
|
||||||
{"Key":"ctrl-x"}
|
{"Key":"ctrl-x"}
|
||||||
{"Get":{"state":"ˇ0\n0\n0 2\n0\n0","mode":"Normal"}}
|
{"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"}}
|
{"Put":{"state":"ˇThe quick brown\nfox jumps"}}
|
||||||
{"Key":"j"}
|
{"Key":"j"}
|
||||||
{"Get":{"state":"The quick brown\nˇfox jumps","mode":"Normal"}}
|
{"Get":{"state":"The quick brown\nˇfox jumps","mode":"Normal"}}
|
||||||
|
5
crates/vim/test_data/test_visual_block_issue_2123.json
Normal file
5
crates/vim/test_data/test_visual_block_issue_2123.json
Normal file
@ -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"}}
|
15
crates/vim/test_data/test_wrapped_motions.json
Normal file
15
crates/vim/test_data/test_wrapped_motions.json
Normal file
@ -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"}}
|
Loading…
Reference in New Issue
Block a user