Merge branch 'main' into test-branch
This commit is contained in:
commit
41590ef64b
92 changed files with 4166 additions and 2520 deletions
|
@ -331,34 +331,91 @@ impl DisplaySnapshot {
|
|||
DisplayPoint(self.blocks_snapshot.max_point())
|
||||
}
|
||||
|
||||
/// Returns text chunks starting at the given display row until the end of the file
|
||||
pub fn text_chunks(&self, display_row: u32) -> impl Iterator<Item = &str> {
|
||||
self.blocks_snapshot
|
||||
.chunks(display_row..self.max_point().row() + 1, false, None)
|
||||
.map(|h| h.text)
|
||||
}
|
||||
|
||||
// Returns text chunks starting at the end of the given display row in reverse until the start of the file
|
||||
pub fn reverse_text_chunks(&self, display_row: u32) -> impl Iterator<Item = &str> {
|
||||
(0..=display_row).into_iter().rev().flat_map(|row| {
|
||||
self.blocks_snapshot
|
||||
.chunks(row..row + 1, false, None)
|
||||
.map(|h| h.text)
|
||||
.collect::<Vec<_>>()
|
||||
.into_iter()
|
||||
.rev()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn chunks(&self, display_rows: Range<u32>, language_aware: bool) -> DisplayChunks<'_> {
|
||||
self.blocks_snapshot
|
||||
.chunks(display_rows, language_aware, Some(&self.text_highlights))
|
||||
}
|
||||
|
||||
pub fn chars_at(&self, point: DisplayPoint) -> impl Iterator<Item = char> + '_ {
|
||||
let mut column = 0;
|
||||
let mut chars = self.text_chunks(point.row()).flat_map(str::chars);
|
||||
while column < point.column() {
|
||||
if let Some(c) = chars.next() {
|
||||
column += c.len_utf8() as u32;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
chars
|
||||
pub fn chars_at(
|
||||
&self,
|
||||
mut point: DisplayPoint,
|
||||
) -> impl Iterator<Item = (char, DisplayPoint)> + '_ {
|
||||
point = DisplayPoint(self.blocks_snapshot.clip_point(point.0, Bias::Left));
|
||||
self.text_chunks(point.row())
|
||||
.flat_map(str::chars)
|
||||
.skip_while({
|
||||
let mut column = 0;
|
||||
move |char| {
|
||||
let at_point = column >= point.column();
|
||||
column += char.len_utf8() as u32;
|
||||
!at_point
|
||||
}
|
||||
})
|
||||
.map(move |ch| {
|
||||
let result = (ch, point);
|
||||
if ch == '\n' {
|
||||
*point.row_mut() += 1;
|
||||
*point.column_mut() = 0;
|
||||
} else {
|
||||
*point.column_mut() += ch.len_utf8() as u32;
|
||||
}
|
||||
result
|
||||
})
|
||||
}
|
||||
|
||||
pub fn reverse_chars_at(
|
||||
&self,
|
||||
mut point: DisplayPoint,
|
||||
) -> impl Iterator<Item = (char, DisplayPoint)> + '_ {
|
||||
point = DisplayPoint(self.blocks_snapshot.clip_point(point.0, Bias::Left));
|
||||
self.reverse_text_chunks(point.row())
|
||||
.flat_map(|chunk| chunk.chars().rev())
|
||||
.skip_while({
|
||||
let mut column = self.line_len(point.row());
|
||||
if self.max_point().row() > point.row() {
|
||||
column += 1;
|
||||
}
|
||||
|
||||
move |char| {
|
||||
let at_point = column <= point.column();
|
||||
column = column.saturating_sub(char.len_utf8() as u32);
|
||||
!at_point
|
||||
}
|
||||
})
|
||||
.map(move |ch| {
|
||||
if ch == '\n' {
|
||||
*point.row_mut() -= 1;
|
||||
*point.column_mut() = self.line_len(point.row());
|
||||
} else {
|
||||
*point.column_mut() = point.column().saturating_sub(ch.len_utf8() as u32);
|
||||
}
|
||||
(ch, point)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn column_to_chars(&self, display_row: u32, target: u32) -> u32 {
|
||||
let mut count = 0;
|
||||
let mut column = 0;
|
||||
for c in self.chars_at(DisplayPoint::new(display_row, 0)) {
|
||||
for (c, _) in self.chars_at(DisplayPoint::new(display_row, 0)) {
|
||||
if column >= target {
|
||||
break;
|
||||
}
|
||||
|
@ -371,7 +428,7 @@ impl DisplaySnapshot {
|
|||
pub fn column_from_chars(&self, display_row: u32, char_count: u32) -> u32 {
|
||||
let mut column = 0;
|
||||
|
||||
for (count, c) in self.chars_at(DisplayPoint::new(display_row, 0)).enumerate() {
|
||||
for (count, (c, _)) in self.chars_at(DisplayPoint::new(display_row, 0)).enumerate() {
|
||||
if c == '\n' || count >= char_count as usize {
|
||||
break;
|
||||
}
|
||||
|
@ -455,7 +512,7 @@ impl DisplaySnapshot {
|
|||
pub fn line_indent(&self, display_row: u32) -> (u32, bool) {
|
||||
let mut indent = 0;
|
||||
let mut is_blank = true;
|
||||
for c in self.chars_at(DisplayPoint::new(display_row, 0)) {
|
||||
for (c, _) in self.chars_at(DisplayPoint::new(display_row, 0)) {
|
||||
if c == ' ' {
|
||||
indent += 1;
|
||||
} else {
|
||||
|
|
|
@ -77,6 +77,7 @@ use util::{post_inc, ResultExt, TryFutureExt};
|
|||
use workspace::{ItemNavHistory, Workspace};
|
||||
|
||||
const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500);
|
||||
const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
|
||||
const MAX_LINE_LEN: usize = 1024;
|
||||
const MIN_NAVIGATION_HISTORY_ROW_DELTA: i64 = 10;
|
||||
const MAX_SELECTION_HISTORY_LEN: usize = 1024;
|
||||
|
@ -239,6 +240,9 @@ pub enum Direction {
|
|||
Next,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct ScrollbarAutoHide(bool);
|
||||
|
||||
pub fn init(cx: &mut MutableAppContext) {
|
||||
cx.add_action(Editor::new_file);
|
||||
cx.add_action(|this: &mut Editor, action: &Scroll, cx| this.set_scroll_position(action.0, cx));
|
||||
|
@ -428,6 +432,8 @@ pub struct Editor {
|
|||
focused: bool,
|
||||
show_local_cursors: bool,
|
||||
show_local_selections: bool,
|
||||
show_scrollbars: bool,
|
||||
hide_scrollbar_task: Option<Task<()>>,
|
||||
blink_epoch: usize,
|
||||
blinking_paused: bool,
|
||||
mode: EditorMode,
|
||||
|
@ -1030,6 +1036,8 @@ impl Editor {
|
|||
focused: false,
|
||||
show_local_cursors: false,
|
||||
show_local_selections: true,
|
||||
show_scrollbars: true,
|
||||
hide_scrollbar_task: None,
|
||||
blink_epoch: 0,
|
||||
blinking_paused: false,
|
||||
mode,
|
||||
|
@ -1062,10 +1070,16 @@ impl Editor {
|
|||
],
|
||||
};
|
||||
this.end_selection(cx);
|
||||
this.make_scrollbar_visible(cx);
|
||||
|
||||
let editor_created_event = EditorCreated(cx.handle());
|
||||
cx.emit_global(editor_created_event);
|
||||
|
||||
if mode == EditorMode::Full {
|
||||
let should_auto_hide_scrollbars = cx.platform().should_auto_hide_scrollbars();
|
||||
cx.set_global(ScrollbarAutoHide(should_auto_hide_scrollbars));
|
||||
}
|
||||
|
||||
this.report_event("open editor", cx);
|
||||
this
|
||||
}
|
||||
|
@ -1182,6 +1196,7 @@ impl Editor {
|
|||
self.scroll_top_anchor = anchor;
|
||||
}
|
||||
|
||||
self.make_scrollbar_visible(cx);
|
||||
self.autoscroll_request.take();
|
||||
hide_hover(self, cx);
|
||||
|
||||
|
@ -1257,7 +1272,7 @@ impl Editor {
|
|||
let max_scroll_top = if matches!(self.mode, EditorMode::AutoHeight { .. }) {
|
||||
(display_map.max_point().row() as f32 - visible_lines + 1.).max(0.)
|
||||
} else {
|
||||
display_map.max_point().row().saturating_sub(1) as f32
|
||||
display_map.max_point().row() as f32
|
||||
};
|
||||
if scroll_position.y() > max_scroll_top {
|
||||
scroll_position.set_y(max_scroll_top);
|
||||
|
@ -4081,7 +4096,7 @@ impl Editor {
|
|||
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
s.move_cursors_with(|map, head, _| {
|
||||
(
|
||||
movement::line_beginning(map, head, true),
|
||||
movement::indented_line_beginning(map, head, true),
|
||||
SelectionGoal::None,
|
||||
)
|
||||
});
|
||||
|
@ -4096,7 +4111,7 @@ impl Editor {
|
|||
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
s.move_heads_with(|map, head, _| {
|
||||
(
|
||||
movement::line_beginning(map, head, action.stop_at_soft_wraps),
|
||||
movement::indented_line_beginning(map, head, action.stop_at_soft_wraps),
|
||||
SelectionGoal::None,
|
||||
)
|
||||
});
|
||||
|
@ -5953,6 +5968,31 @@ impl Editor {
|
|||
self.show_local_cursors && self.focused
|
||||
}
|
||||
|
||||
pub fn show_scrollbars(&self) -> bool {
|
||||
self.show_scrollbars
|
||||
}
|
||||
|
||||
fn make_scrollbar_visible(&mut self, cx: &mut ViewContext<Self>) {
|
||||
if !self.show_scrollbars {
|
||||
self.show_scrollbars = true;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
if cx.default_global::<ScrollbarAutoHide>().0 {
|
||||
self.hide_scrollbar_task = Some(cx.spawn_weak(|this, mut cx| async move {
|
||||
Timer::after(SCROLLBAR_SHOW_INTERVAL).await;
|
||||
if let Some(this) = this.upgrade(&cx) {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.show_scrollbars = false;
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
}));
|
||||
} else {
|
||||
self.hide_scrollbar_task = None;
|
||||
}
|
||||
}
|
||||
|
||||
fn on_buffer_changed(&mut self, _: ModelHandle<MultiBuffer>, cx: &mut ViewContext<Self>) {
|
||||
cx.notify();
|
||||
}
|
||||
|
|
|
@ -1,20 +1,22 @@
|
|||
use std::{cell::RefCell, rc::Rc, time::Instant};
|
||||
|
||||
use futures::StreamExt;
|
||||
use indoc::indoc;
|
||||
use unindent::Unindent;
|
||||
|
||||
use super::*;
|
||||
use crate::test::{
|
||||
assert_text_with_selections, build_editor, select_ranges, EditorLspTestContext,
|
||||
EditorTestContext,
|
||||
assert_text_with_selections, build_editor, editor_lsp_test_context::EditorLspTestContext,
|
||||
editor_test_context::EditorTestContext, select_ranges,
|
||||
};
|
||||
use futures::StreamExt;
|
||||
use gpui::{
|
||||
geometry::rect::RectF,
|
||||
platform::{WindowBounds, WindowOptions},
|
||||
};
|
||||
use indoc::indoc;
|
||||
use language::{FakeLspAdapter, LanguageConfig, LanguageRegistry};
|
||||
use project::FakeFs;
|
||||
use rope::point::Point;
|
||||
use settings::EditorSettings;
|
||||
use std::{cell::RefCell, rc::Rc, time::Instant};
|
||||
use unindent::Unindent;
|
||||
use util::{
|
||||
assert_set_eq,
|
||||
test::{marked_text_ranges, marked_text_ranges_by, sample_text, TextRangeMarker},
|
||||
|
|
|
@ -44,7 +44,7 @@ use std::{
|
|||
cmp::{self, Ordering},
|
||||
fmt::Write,
|
||||
iter,
|
||||
ops::Range,
|
||||
ops::{DerefMut, Range},
|
||||
sync::Arc,
|
||||
};
|
||||
use theme::DiffStyle;
|
||||
|
@ -455,7 +455,6 @@ impl EditorElement {
|
|||
let bounds = gutter_bounds.union_rect(text_bounds);
|
||||
let scroll_top =
|
||||
layout.position_map.snapshot.scroll_position().y() * layout.position_map.line_height;
|
||||
let editor = self.view(cx.app);
|
||||
cx.scene.push_quad(Quad {
|
||||
bounds: gutter_bounds,
|
||||
background: Some(self.style.gutter_background),
|
||||
|
@ -469,7 +468,7 @@ impl EditorElement {
|
|||
corner_radius: 0.,
|
||||
});
|
||||
|
||||
if let EditorMode::Full = editor.mode {
|
||||
if let EditorMode::Full = layout.mode {
|
||||
let mut active_rows = layout.active_rows.iter().peekable();
|
||||
while let Some((start_row, contains_non_empty_selection)) = active_rows.next() {
|
||||
let mut end_row = *start_row;
|
||||
|
@ -753,7 +752,7 @@ impl EditorElement {
|
|||
.snapshot
|
||||
.chars_at(cursor_position)
|
||||
.next()
|
||||
.and_then(|character| {
|
||||
.and_then(|(character, _)| {
|
||||
let font_id =
|
||||
cursor_row_layout.font_for_index(cursor_column)?;
|
||||
let text = character.to_string();
|
||||
|
@ -910,6 +909,119 @@ impl EditorElement {
|
|||
cx.scene.pop_layer();
|
||||
}
|
||||
|
||||
fn paint_scrollbar(&mut self, bounds: RectF, layout: &mut LayoutState, cx: &mut PaintContext) {
|
||||
enum ScrollbarMouseHandlers {}
|
||||
if layout.mode != EditorMode::Full {
|
||||
return;
|
||||
}
|
||||
|
||||
let view = self.view.clone();
|
||||
let style = &self.style.theme.scrollbar;
|
||||
|
||||
let top = bounds.min_y();
|
||||
let bottom = bounds.max_y();
|
||||
let right = bounds.max_x();
|
||||
let left = right - style.width;
|
||||
let row_range = &layout.scrollbar_row_range;
|
||||
let max_row = layout.max_row as f32 + (row_range.end - row_range.start);
|
||||
|
||||
let mut height = bounds.height();
|
||||
let mut first_row_y_offset = 0.0;
|
||||
|
||||
// Impose a minimum height on the scrollbar thumb
|
||||
let min_thumb_height =
|
||||
style.min_height_factor * cx.font_cache.line_height(self.style.text.font_size);
|
||||
let thumb_height = (row_range.end - row_range.start) * height / max_row;
|
||||
if thumb_height < min_thumb_height {
|
||||
first_row_y_offset = (min_thumb_height - thumb_height) / 2.0;
|
||||
height -= min_thumb_height - thumb_height;
|
||||
}
|
||||
|
||||
let y_for_row = |row: f32| -> f32 { top + first_row_y_offset + row * height / max_row };
|
||||
|
||||
let thumb_top = y_for_row(row_range.start) - first_row_y_offset;
|
||||
let thumb_bottom = y_for_row(row_range.end) + first_row_y_offset;
|
||||
let track_bounds = RectF::from_points(vec2f(left, top), vec2f(right, bottom));
|
||||
let thumb_bounds = RectF::from_points(vec2f(left, thumb_top), vec2f(right, thumb_bottom));
|
||||
|
||||
if layout.show_scrollbars {
|
||||
cx.scene.push_quad(Quad {
|
||||
bounds: track_bounds,
|
||||
border: style.track.border,
|
||||
background: style.track.background_color,
|
||||
..Default::default()
|
||||
});
|
||||
cx.scene.push_quad(Quad {
|
||||
bounds: thumb_bounds,
|
||||
border: style.thumb.border,
|
||||
background: style.thumb.background_color,
|
||||
corner_radius: style.thumb.corner_radius,
|
||||
});
|
||||
}
|
||||
|
||||
cx.scene.push_cursor_region(CursorRegion {
|
||||
bounds: track_bounds,
|
||||
style: CursorStyle::Arrow,
|
||||
});
|
||||
cx.scene.push_mouse_region(
|
||||
MouseRegion::new::<ScrollbarMouseHandlers>(view.id(), view.id(), track_bounds)
|
||||
.on_move({
|
||||
let view = view.clone();
|
||||
move |_, cx| {
|
||||
if let Some(view) = view.upgrade(cx.deref_mut()) {
|
||||
view.update(cx.deref_mut(), |view, cx| {
|
||||
view.make_scrollbar_visible(cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
.on_down(MouseButton::Left, {
|
||||
let view = view.clone();
|
||||
let row_range = row_range.clone();
|
||||
move |e, cx| {
|
||||
let y = e.position.y();
|
||||
if let Some(view) = view.upgrade(cx.deref_mut()) {
|
||||
view.update(cx.deref_mut(), |view, cx| {
|
||||
if y < thumb_top || thumb_bottom < y {
|
||||
let center_row =
|
||||
((y - top) * max_row as f32 / height).round() as u32;
|
||||
let top_row = center_row.saturating_sub(
|
||||
(row_range.end - row_range.start) as u32 / 2,
|
||||
);
|
||||
let mut position = view.scroll_position(cx);
|
||||
position.set_y(top_row as f32);
|
||||
view.set_scroll_position(position, cx);
|
||||
} else {
|
||||
view.make_scrollbar_visible(cx);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
.on_drag(MouseButton::Left, {
|
||||
let view = view.clone();
|
||||
move |e, cx| {
|
||||
let y = e.prev_mouse_position.y();
|
||||
let new_y = e.position.y();
|
||||
if thumb_top < y && y < thumb_bottom {
|
||||
if let Some(view) = view.upgrade(cx.deref_mut()) {
|
||||
view.update(cx.deref_mut(), |view, cx| {
|
||||
let mut position = view.scroll_position(cx);
|
||||
position.set_y(
|
||||
position.y() + (new_y - y) * (max_row as f32) / height,
|
||||
);
|
||||
if position.y() < 0.0 {
|
||||
position.set_y(0.);
|
||||
}
|
||||
view.set_scroll_position(position, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn paint_highlighted_range(
|
||||
&self,
|
||||
|
@ -1470,13 +1582,11 @@ impl Element for EditorElement {
|
|||
// The scroll position is a fractional point, the whole number of which represents
|
||||
// the top of the window in terms of display rows.
|
||||
let start_row = scroll_position.y() as u32;
|
||||
let scroll_top = scroll_position.y() * line_height;
|
||||
let visible_row_count = (size.y() / line_height).ceil() as u32;
|
||||
let max_row = snapshot.max_point().row();
|
||||
|
||||
// Add 1 to ensure selections bleed off screen
|
||||
let end_row = 1 + cmp::min(
|
||||
((scroll_top + size.y()) / line_height).ceil() as u32,
|
||||
snapshot.max_point().row(),
|
||||
);
|
||||
let end_row = 1 + cmp::min(start_row + visible_row_count, max_row);
|
||||
|
||||
let start_anchor = if start_row == 0 {
|
||||
Anchor::min()
|
||||
|
@ -1485,7 +1595,7 @@ impl Element for EditorElement {
|
|||
.buffer_snapshot
|
||||
.anchor_before(DisplayPoint::new(start_row, 0).to_offset(&snapshot, Bias::Left))
|
||||
};
|
||||
let end_anchor = if end_row > snapshot.max_point().row() {
|
||||
let end_anchor = if end_row > max_row {
|
||||
Anchor::max()
|
||||
} else {
|
||||
snapshot
|
||||
|
@ -1497,6 +1607,7 @@ impl Element for EditorElement {
|
|||
let mut active_rows = BTreeMap::new();
|
||||
let mut highlighted_rows = None;
|
||||
let mut highlighted_ranges = Vec::new();
|
||||
let mut show_scrollbars = false;
|
||||
self.update_view(cx.app, |view, cx| {
|
||||
let display_map = view.display_map.update(cx, |map, cx| map.snapshot(cx));
|
||||
|
||||
|
@ -1557,6 +1668,8 @@ impl Element for EditorElement {
|
|||
.collect(),
|
||||
));
|
||||
}
|
||||
|
||||
show_scrollbars = view.show_scrollbars();
|
||||
});
|
||||
|
||||
let line_number_layouts =
|
||||
|
@ -1567,6 +1680,9 @@ impl Element for EditorElement {
|
|||
.git_diff_hunks_in_range(start_row..end_row)
|
||||
.collect();
|
||||
|
||||
let scrollbar_row_range =
|
||||
scroll_position.y()..(scroll_position.y() + visible_row_count as f32);
|
||||
|
||||
let mut max_visible_line_width = 0.0;
|
||||
let line_layouts = self.layout_lines(start_row..end_row, &snapshot, cx);
|
||||
for line in &line_layouts {
|
||||
|
@ -1600,10 +1716,9 @@ impl Element for EditorElement {
|
|||
cx,
|
||||
);
|
||||
|
||||
let max_row = snapshot.max_point().row();
|
||||
let scroll_max = vec2f(
|
||||
((scroll_width - text_size.x()) / em_width).max(0.0),
|
||||
max_row.saturating_sub(1) as f32,
|
||||
max_row as f32,
|
||||
);
|
||||
|
||||
self.update_view(cx.app, |view, cx| {
|
||||
|
@ -1630,6 +1745,7 @@ impl Element for EditorElement {
|
|||
let mut context_menu = None;
|
||||
let mut code_actions_indicator = None;
|
||||
let mut hover = None;
|
||||
let mut mode = EditorMode::Full;
|
||||
cx.render(&self.view.upgrade(cx).unwrap(), |view, cx| {
|
||||
let newest_selection_head = view
|
||||
.selections
|
||||
|
@ -1651,6 +1767,7 @@ impl Element for EditorElement {
|
|||
|
||||
let visible_rows = start_row..start_row + line_layouts.len() as u32;
|
||||
hover = view.hover_state.render(&snapshot, &style, visible_rows, cx);
|
||||
mode = view.mode;
|
||||
});
|
||||
|
||||
if let Some((_, context_menu)) = context_menu.as_mut() {
|
||||
|
@ -1698,6 +1815,7 @@ impl Element for EditorElement {
|
|||
(
|
||||
size,
|
||||
LayoutState {
|
||||
mode,
|
||||
position_map: Arc::new(PositionMap {
|
||||
size,
|
||||
scroll_max,
|
||||
|
@ -1710,6 +1828,9 @@ impl Element for EditorElement {
|
|||
gutter_size,
|
||||
gutter_padding,
|
||||
text_size,
|
||||
scrollbar_row_range,
|
||||
show_scrollbars,
|
||||
max_row,
|
||||
gutter_margin,
|
||||
active_rows,
|
||||
highlighted_rows,
|
||||
|
@ -1757,11 +1878,12 @@ impl Element for EditorElement {
|
|||
}
|
||||
self.paint_text(text_bounds, visible_bounds, layout, cx);
|
||||
|
||||
cx.scene.push_layer(Some(bounds));
|
||||
if !layout.blocks.is_empty() {
|
||||
cx.scene.push_layer(Some(bounds));
|
||||
self.paint_blocks(bounds, visible_bounds, layout, cx);
|
||||
cx.scene.pop_layer();
|
||||
}
|
||||
self.paint_scrollbar(bounds, layout, cx);
|
||||
cx.scene.pop_layer();
|
||||
|
||||
cx.scene.pop_layer();
|
||||
}
|
||||
|
@ -1847,12 +1969,16 @@ pub struct LayoutState {
|
|||
gutter_padding: f32,
|
||||
gutter_margin: f32,
|
||||
text_size: Vector2F,
|
||||
mode: EditorMode,
|
||||
active_rows: BTreeMap<u32, bool>,
|
||||
highlighted_rows: Option<Range<u32>>,
|
||||
line_number_layouts: Vec<Option<text_layout::Line>>,
|
||||
blocks: Vec<BlockLayout>,
|
||||
highlighted_ranges: Vec<(Range<DisplayPoint>, Color)>,
|
||||
selections: Vec<(ReplicaId, Vec<SelectionLayout>)>,
|
||||
scrollbar_row_range: Range<f32>,
|
||||
show_scrollbars: bool,
|
||||
max_row: u32,
|
||||
context_menu: Option<(DisplayPoint, ElementBox)>,
|
||||
diff_hunks: Vec<DiffHunk<u32>>,
|
||||
code_actions_indicator: Option<(u32, ElementBox)>,
|
||||
|
|
|
@ -32,8 +32,9 @@ pub fn refresh_matching_bracket_highlights(editor: &mut Editor, cx: &mut ViewCon
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::test::editor_lsp_test_context::EditorLspTestContext;
|
||||
|
||||
use super::*;
|
||||
use crate::test::EditorLspTestContext;
|
||||
use indoc::indoc;
|
||||
use language::{BracketPair, Language, LanguageConfig};
|
||||
|
||||
|
|
|
@ -427,13 +427,13 @@ impl DiagnosticPopover {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use futures::StreamExt;
|
||||
use indoc::indoc;
|
||||
|
||||
use language::{Diagnostic, DiagnosticSet};
|
||||
use project::HoverBlock;
|
||||
use smol::stream::StreamExt;
|
||||
|
||||
use crate::test::EditorLspTestContext;
|
||||
use crate::test::editor_lsp_test_context::EditorLspTestContext;
|
||||
|
||||
use super::*;
|
||||
|
||||
|
|
|
@ -400,7 +400,7 @@ mod tests {
|
|||
use indoc::indoc;
|
||||
use lsp::request::{GotoDefinition, GotoTypeDefinition};
|
||||
|
||||
use crate::test::EditorLspTestContext;
|
||||
use crate::test::editor_lsp_test_context::EditorLspTestContext;
|
||||
|
||||
use super::*;
|
||||
|
||||
|
|
|
@ -70,8 +70,9 @@ pub fn deploy_context_menu(
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::test::editor_lsp_test_context::EditorLspTestContext;
|
||||
|
||||
use super::*;
|
||||
use crate::test::EditorLspTestContext;
|
||||
use indoc::indoc;
|
||||
|
||||
#[gpui::test]
|
||||
|
|
|
@ -102,6 +102,22 @@ pub fn line_beginning(
|
|||
map: &DisplaySnapshot,
|
||||
display_point: DisplayPoint,
|
||||
stop_at_soft_boundaries: bool,
|
||||
) -> DisplayPoint {
|
||||
let point = display_point.to_point(map);
|
||||
let soft_line_start = map.clip_point(DisplayPoint::new(display_point.row(), 0), Bias::Right);
|
||||
let line_start = map.prev_line_boundary(point).1;
|
||||
|
||||
if stop_at_soft_boundaries && display_point != soft_line_start {
|
||||
soft_line_start
|
||||
} else {
|
||||
line_start
|
||||
}
|
||||
}
|
||||
|
||||
pub fn indented_line_beginning(
|
||||
map: &DisplaySnapshot,
|
||||
display_point: DisplayPoint,
|
||||
stop_at_soft_boundaries: bool,
|
||||
) -> DisplayPoint {
|
||||
let point = display_point.to_point(map);
|
||||
let soft_line_start = map.clip_point(DisplayPoint::new(display_point.row(), 0), Bias::Right);
|
||||
|
@ -168,54 +184,79 @@ pub fn next_subword_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPo
|
|||
})
|
||||
}
|
||||
|
||||
/// Scans for a boundary from the start of each line preceding the given end point until a boundary
|
||||
/// is found, indicated by the given predicate returning true. The predicate is called with the
|
||||
/// character to the left and right of the candidate boundary location, and will be called with `\n`
|
||||
/// characters indicating the start or end of a line. If the predicate returns true multiple times
|
||||
/// on a line, the *rightmost* boundary is returned.
|
||||
/// Scans for a boundary preceding the given start point `from` until a boundary is found, indicated by the
|
||||
/// given predicate returning true. The predicate is called with the character to the left and right
|
||||
/// of the candidate boundary location, and will be called with `\n` characters indicating the start
|
||||
/// or end of a line.
|
||||
pub fn find_preceding_boundary(
|
||||
map: &DisplaySnapshot,
|
||||
end: DisplayPoint,
|
||||
from: DisplayPoint,
|
||||
mut is_boundary: impl FnMut(char, char) -> bool,
|
||||
) -> DisplayPoint {
|
||||
let mut point = end;
|
||||
loop {
|
||||
*point.column_mut() = 0;
|
||||
if point.row() > 0 {
|
||||
if let Some(indent) = map.soft_wrap_indent(point.row() - 1) {
|
||||
*point.column_mut() = indent;
|
||||
let mut start_column = 0;
|
||||
let mut soft_wrap_row = from.row() + 1;
|
||||
|
||||
let mut prev = None;
|
||||
for (ch, point) in map.reverse_chars_at(from) {
|
||||
// Recompute soft_wrap_indent if the row has changed
|
||||
if point.row() != soft_wrap_row {
|
||||
soft_wrap_row = point.row();
|
||||
|
||||
if point.row() == 0 {
|
||||
start_column = 0;
|
||||
} else if let Some(indent) = map.soft_wrap_indent(point.row() - 1) {
|
||||
start_column = indent;
|
||||
}
|
||||
}
|
||||
|
||||
let mut boundary = None;
|
||||
let mut prev_ch = if point.is_zero() { None } else { Some('\n') };
|
||||
for ch in map.chars_at(point) {
|
||||
if point >= end {
|
||||
break;
|
||||
}
|
||||
|
||||
if let Some(prev_ch) = prev_ch {
|
||||
if is_boundary(prev_ch, ch) {
|
||||
boundary = Some(point);
|
||||
}
|
||||
}
|
||||
|
||||
if ch == '\n' {
|
||||
break;
|
||||
}
|
||||
|
||||
prev_ch = Some(ch);
|
||||
*point.column_mut() += ch.len_utf8() as u32;
|
||||
// If the current point is in the soft_wrap, skip comparing it
|
||||
if point.column() < start_column {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(boundary) = boundary {
|
||||
return boundary;
|
||||
} else if point.row() == 0 {
|
||||
return DisplayPoint::zero();
|
||||
} else {
|
||||
*point.row_mut() -= 1;
|
||||
if let Some((prev_ch, prev_point)) = prev {
|
||||
if is_boundary(ch, prev_ch) {
|
||||
return prev_point;
|
||||
}
|
||||
}
|
||||
|
||||
prev = Some((ch, point));
|
||||
}
|
||||
DisplayPoint::zero()
|
||||
}
|
||||
|
||||
/// Scans for a boundary preceding the given start point `from` until a boundary is found, indicated by the
|
||||
/// given predicate returning true. The predicate is called with the character to the left and right
|
||||
/// of the candidate boundary location, and will be called with `\n` characters indicating the start
|
||||
/// or end of a line. If no boundary is found, the start of the line is returned.
|
||||
pub fn find_preceding_boundary_in_line(
|
||||
map: &DisplaySnapshot,
|
||||
from: DisplayPoint,
|
||||
mut is_boundary: impl FnMut(char, char) -> bool,
|
||||
) -> DisplayPoint {
|
||||
let mut start_column = 0;
|
||||
if from.row() > 0 {
|
||||
if let Some(indent) = map.soft_wrap_indent(from.row() - 1) {
|
||||
start_column = indent;
|
||||
}
|
||||
}
|
||||
|
||||
let mut prev = None;
|
||||
for (ch, point) in map.reverse_chars_at(from) {
|
||||
if let Some((prev_ch, prev_point)) = prev {
|
||||
if is_boundary(ch, prev_ch) {
|
||||
return prev_point;
|
||||
}
|
||||
}
|
||||
|
||||
if ch == '\n' || point.column() < start_column {
|
||||
break;
|
||||
}
|
||||
|
||||
prev = Some((ch, point));
|
||||
}
|
||||
|
||||
prev.map(|(_, point)| point).unwrap_or(from)
|
||||
}
|
||||
|
||||
/// Scans for a boundary following the given start point until a boundary is found, indicated by the
|
||||
|
@ -224,26 +265,48 @@ pub fn find_preceding_boundary(
|
|||
/// or end of a line.
|
||||
pub fn find_boundary(
|
||||
map: &DisplaySnapshot,
|
||||
mut point: DisplayPoint,
|
||||
from: DisplayPoint,
|
||||
mut is_boundary: impl FnMut(char, char) -> bool,
|
||||
) -> DisplayPoint {
|
||||
let mut prev_ch = None;
|
||||
for ch in map.chars_at(point) {
|
||||
for (ch, point) in map.chars_at(from) {
|
||||
if let Some(prev_ch) = prev_ch {
|
||||
if is_boundary(prev_ch, ch) {
|
||||
break;
|
||||
return map.clip_point(point, Bias::Right);
|
||||
}
|
||||
}
|
||||
|
||||
if ch == '\n' {
|
||||
*point.row_mut() += 1;
|
||||
*point.column_mut() = 0;
|
||||
} else {
|
||||
*point.column_mut() += ch.len_utf8() as u32;
|
||||
}
|
||||
prev_ch = Some(ch);
|
||||
}
|
||||
map.clip_point(point, Bias::Right)
|
||||
map.clip_point(map.max_point(), Bias::Right)
|
||||
}
|
||||
|
||||
/// Scans for a boundary following the given start point until a boundary is found, indicated by the
|
||||
/// given predicate returning true. The predicate is called with the character to the left and right
|
||||
/// of the candidate boundary location, and will be called with `\n` characters indicating the start
|
||||
/// or end of a line. If no boundary is found, the end of the line is returned
|
||||
pub fn find_boundary_in_line(
|
||||
map: &DisplaySnapshot,
|
||||
from: DisplayPoint,
|
||||
mut is_boundary: impl FnMut(char, char) -> bool,
|
||||
) -> DisplayPoint {
|
||||
let mut prev = None;
|
||||
for (ch, point) in map.chars_at(from) {
|
||||
if let Some((prev_ch, _)) = prev {
|
||||
if is_boundary(prev_ch, ch) {
|
||||
return map.clip_point(point, Bias::Right);
|
||||
}
|
||||
}
|
||||
|
||||
prev = Some((ch, point));
|
||||
|
||||
if ch == '\n' {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Return the last position checked so that we give a point right before the newline or eof.
|
||||
map.clip_point(prev.map(|(_, point)| point).unwrap_or(from), Bias::Right)
|
||||
}
|
||||
|
||||
pub fn is_inside_word(map: &DisplaySnapshot, point: DisplayPoint) -> bool {
|
||||
|
|
|
@ -1,28 +1,14 @@
|
|||
pub mod editor_lsp_test_context;
|
||||
pub mod editor_test_context;
|
||||
|
||||
use crate::{
|
||||
display_map::{DisplayMap, DisplaySnapshot, ToDisplayPoint},
|
||||
multi_buffer::ToPointUtf16,
|
||||
AnchorRangeExt, Autoscroll, DisplayPoint, Editor, EditorMode, MultiBuffer, ToPoint,
|
||||
DisplayPoint, Editor, EditorMode, MultiBuffer,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use futures::{Future, StreamExt};
|
||||
use gpui::{
|
||||
json, keymap::Keystroke, AppContext, ModelContext, ModelHandle, ViewContext, ViewHandle,
|
||||
};
|
||||
use indoc::indoc;
|
||||
use language::{point_to_lsp, Buffer, BufferSnapshot, FakeLspAdapter, Language, LanguageConfig};
|
||||
use lsp::{notification, request};
|
||||
use project::Project;
|
||||
use settings::Settings;
|
||||
use std::{
|
||||
any::TypeId,
|
||||
ops::{Deref, DerefMut, Range},
|
||||
sync::Arc,
|
||||
};
|
||||
use util::{
|
||||
assert_set_eq, set_eq,
|
||||
test::{generate_marked_text, marked_text_offsets, marked_text_ranges},
|
||||
};
|
||||
use workspace::{pane, AppState, Workspace, WorkspaceHandle};
|
||||
|
||||
use gpui::{ModelHandle, ViewContext};
|
||||
|
||||
use util::test::{marked_text_offsets, marked_text_ranges};
|
||||
|
||||
#[cfg(test)]
|
||||
#[ctor::ctor]
|
||||
|
@ -80,430 +66,3 @@ pub(crate) fn build_editor(
|
|||
) -> Editor {
|
||||
Editor::new(EditorMode::Full, buffer, None, None, cx)
|
||||
}
|
||||
|
||||
pub struct EditorTestContext<'a> {
|
||||
pub cx: &'a mut gpui::TestAppContext,
|
||||
pub window_id: usize,
|
||||
pub editor: ViewHandle<Editor>,
|
||||
}
|
||||
|
||||
impl<'a> EditorTestContext<'a> {
|
||||
pub fn new(cx: &'a mut gpui::TestAppContext) -> EditorTestContext<'a> {
|
||||
let (window_id, editor) = cx.update(|cx| {
|
||||
cx.set_global(Settings::test(cx));
|
||||
crate::init(cx);
|
||||
|
||||
let (window_id, editor) = cx.add_window(Default::default(), |cx| {
|
||||
build_editor(MultiBuffer::build_simple("", cx), cx)
|
||||
});
|
||||
|
||||
editor.update(cx, |_, cx| cx.focus_self());
|
||||
|
||||
(window_id, editor)
|
||||
});
|
||||
|
||||
Self {
|
||||
cx,
|
||||
window_id,
|
||||
editor,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn condition(
|
||||
&self,
|
||||
predicate: impl FnMut(&Editor, &AppContext) -> bool,
|
||||
) -> impl Future<Output = ()> {
|
||||
self.editor.condition(self.cx, predicate)
|
||||
}
|
||||
|
||||
pub fn editor<F, T>(&self, read: F) -> T
|
||||
where
|
||||
F: FnOnce(&Editor, &AppContext) -> T,
|
||||
{
|
||||
self.editor.read_with(self.cx, read)
|
||||
}
|
||||
|
||||
pub fn update_editor<F, T>(&mut self, update: F) -> T
|
||||
where
|
||||
F: FnOnce(&mut Editor, &mut ViewContext<Editor>) -> T,
|
||||
{
|
||||
self.editor.update(self.cx, update)
|
||||
}
|
||||
|
||||
pub fn multibuffer<F, T>(&self, read: F) -> T
|
||||
where
|
||||
F: FnOnce(&MultiBuffer, &AppContext) -> T,
|
||||
{
|
||||
self.editor(|editor, cx| read(editor.buffer().read(cx), cx))
|
||||
}
|
||||
|
||||
pub fn update_multibuffer<F, T>(&mut self, update: F) -> T
|
||||
where
|
||||
F: FnOnce(&mut MultiBuffer, &mut ModelContext<MultiBuffer>) -> T,
|
||||
{
|
||||
self.update_editor(|editor, cx| editor.buffer().update(cx, update))
|
||||
}
|
||||
|
||||
pub fn buffer_text(&self) -> String {
|
||||
self.multibuffer(|buffer, cx| buffer.snapshot(cx).text())
|
||||
}
|
||||
|
||||
pub fn buffer<F, T>(&self, read: F) -> T
|
||||
where
|
||||
F: FnOnce(&Buffer, &AppContext) -> T,
|
||||
{
|
||||
self.multibuffer(|multibuffer, cx| {
|
||||
let buffer = multibuffer.as_singleton().unwrap().read(cx);
|
||||
read(buffer, cx)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn update_buffer<F, T>(&mut self, update: F) -> T
|
||||
where
|
||||
F: FnOnce(&mut Buffer, &mut ModelContext<Buffer>) -> T,
|
||||
{
|
||||
self.update_multibuffer(|multibuffer, cx| {
|
||||
let buffer = multibuffer.as_singleton().unwrap();
|
||||
buffer.update(cx, update)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn buffer_snapshot(&self) -> BufferSnapshot {
|
||||
self.buffer(|buffer, _| buffer.snapshot())
|
||||
}
|
||||
|
||||
pub fn simulate_keystroke(&mut self, keystroke_text: &str) {
|
||||
let keystroke = Keystroke::parse(keystroke_text).unwrap();
|
||||
self.cx.dispatch_keystroke(self.window_id, keystroke, false);
|
||||
}
|
||||
|
||||
pub fn simulate_keystrokes<const COUNT: usize>(&mut self, keystroke_texts: [&str; COUNT]) {
|
||||
for keystroke_text in keystroke_texts.into_iter() {
|
||||
self.simulate_keystroke(keystroke_text);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ranges(&self, marked_text: &str) -> Vec<Range<usize>> {
|
||||
let (unmarked_text, ranges) = marked_text_ranges(marked_text, false);
|
||||
assert_eq!(self.buffer_text(), unmarked_text);
|
||||
ranges
|
||||
}
|
||||
|
||||
pub fn display_point(&mut self, marked_text: &str) -> DisplayPoint {
|
||||
let ranges = self.ranges(marked_text);
|
||||
let snapshot = self
|
||||
.editor
|
||||
.update(self.cx, |editor, cx| editor.snapshot(cx));
|
||||
ranges[0].start.to_display_point(&snapshot)
|
||||
}
|
||||
|
||||
// Returns anchors for the current buffer using `«` and `»`
|
||||
pub fn text_anchor_range(&self, marked_text: &str) -> Range<language::Anchor> {
|
||||
let ranges = self.ranges(marked_text);
|
||||
let snapshot = self.buffer_snapshot();
|
||||
snapshot.anchor_before(ranges[0].start)..snapshot.anchor_after(ranges[0].end)
|
||||
}
|
||||
|
||||
/// Change the editor's text and selections using a string containing
|
||||
/// embedded range markers that represent the ranges and directions of
|
||||
/// each selection.
|
||||
///
|
||||
/// See the `util::test::marked_text_ranges` function for more information.
|
||||
pub fn set_state(&mut self, marked_text: &str) {
|
||||
let (unmarked_text, selection_ranges) = marked_text_ranges(marked_text, true);
|
||||
self.editor.update(self.cx, |editor, cx| {
|
||||
editor.set_text(unmarked_text, cx);
|
||||
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
s.select_ranges(selection_ranges)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// Make an assertion about the editor's text and the ranges and directions
|
||||
/// of its selections using a string containing embedded range markers.
|
||||
///
|
||||
/// See the `util::test::marked_text_ranges` function for more information.
|
||||
pub fn assert_editor_state(&mut self, marked_text: &str) {
|
||||
let (unmarked_text, expected_selections) = marked_text_ranges(marked_text, true);
|
||||
let buffer_text = self.buffer_text();
|
||||
assert_eq!(
|
||||
buffer_text, unmarked_text,
|
||||
"Unmarked text doesn't match buffer text"
|
||||
);
|
||||
self.assert_selections(expected_selections, marked_text.to_string())
|
||||
}
|
||||
|
||||
pub fn assert_editor_background_highlights<Tag: 'static>(&mut self, marked_text: &str) {
|
||||
let expected_ranges = self.ranges(marked_text);
|
||||
let actual_ranges: Vec<Range<usize>> = self.update_editor(|editor, cx| {
|
||||
let snapshot = editor.snapshot(cx);
|
||||
editor
|
||||
.background_highlights
|
||||
.get(&TypeId::of::<Tag>())
|
||||
.map(|h| h.1.clone())
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(|range| range.to_offset(&snapshot.buffer_snapshot))
|
||||
.collect()
|
||||
});
|
||||
assert_set_eq!(actual_ranges, expected_ranges);
|
||||
}
|
||||
|
||||
pub fn assert_editor_text_highlights<Tag: ?Sized + 'static>(&mut self, marked_text: &str) {
|
||||
let expected_ranges = self.ranges(marked_text);
|
||||
let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx));
|
||||
let actual_ranges: Vec<Range<usize>> = snapshot
|
||||
.highlight_ranges::<Tag>()
|
||||
.map(|ranges| ranges.as_ref().clone().1)
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(|range| range.to_offset(&snapshot.buffer_snapshot))
|
||||
.collect();
|
||||
assert_set_eq!(actual_ranges, expected_ranges);
|
||||
}
|
||||
|
||||
pub fn assert_editor_selections(&mut self, expected_selections: Vec<Range<usize>>) {
|
||||
let expected_marked_text =
|
||||
generate_marked_text(&self.buffer_text(), &expected_selections, true);
|
||||
self.assert_selections(expected_selections, expected_marked_text)
|
||||
}
|
||||
|
||||
fn assert_selections(
|
||||
&mut self,
|
||||
expected_selections: Vec<Range<usize>>,
|
||||
expected_marked_text: String,
|
||||
) {
|
||||
let actual_selections = self
|
||||
.editor
|
||||
.read_with(self.cx, |editor, cx| editor.selections.all::<usize>(cx))
|
||||
.into_iter()
|
||||
.map(|s| {
|
||||
if s.reversed {
|
||||
s.end..s.start
|
||||
} else {
|
||||
s.start..s.end
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let actual_marked_text =
|
||||
generate_marked_text(&self.buffer_text(), &actual_selections, true);
|
||||
if expected_selections != actual_selections {
|
||||
panic!(
|
||||
indoc! {"
|
||||
Editor has unexpected selections.
|
||||
|
||||
Expected selections:
|
||||
{}
|
||||
|
||||
Actual selections:
|
||||
{}
|
||||
"},
|
||||
expected_marked_text, actual_marked_text,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Deref for EditorTestContext<'a> {
|
||||
type Target = gpui::TestAppContext;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.cx
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> DerefMut for EditorTestContext<'a> {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.cx
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EditorLspTestContext<'a> {
|
||||
pub cx: EditorTestContext<'a>,
|
||||
pub lsp: lsp::FakeLanguageServer,
|
||||
pub workspace: ViewHandle<Workspace>,
|
||||
pub buffer_lsp_url: lsp::Url,
|
||||
}
|
||||
|
||||
impl<'a> EditorLspTestContext<'a> {
|
||||
pub async fn new(
|
||||
mut language: Language,
|
||||
capabilities: lsp::ServerCapabilities,
|
||||
cx: &'a mut gpui::TestAppContext,
|
||||
) -> EditorLspTestContext<'a> {
|
||||
use json::json;
|
||||
|
||||
cx.update(|cx| {
|
||||
crate::init(cx);
|
||||
pane::init(cx);
|
||||
});
|
||||
|
||||
let params = cx.update(AppState::test);
|
||||
|
||||
let file_name = format!(
|
||||
"file.{}",
|
||||
language
|
||||
.path_suffixes()
|
||||
.first()
|
||||
.unwrap_or(&"txt".to_string())
|
||||
);
|
||||
|
||||
let mut fake_servers = language
|
||||
.set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
|
||||
capabilities,
|
||||
..Default::default()
|
||||
}))
|
||||
.await;
|
||||
|
||||
let project = Project::test(params.fs.clone(), [], cx).await;
|
||||
project.update(cx, |project, _| project.languages().add(Arc::new(language)));
|
||||
|
||||
params
|
||||
.fs
|
||||
.as_fake()
|
||||
.insert_tree("/root", json!({ "dir": { file_name: "" }}))
|
||||
.await;
|
||||
|
||||
let (window_id, workspace) =
|
||||
cx.add_window(|cx| Workspace::new(project.clone(), |_, _| unimplemented!(), cx));
|
||||
project
|
||||
.update(cx, |project, cx| {
|
||||
project.find_or_create_local_worktree("/root", true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx))
|
||||
.await;
|
||||
|
||||
let file = cx.read(|cx| workspace.file_project_paths(cx)[0].clone());
|
||||
let item = workspace
|
||||
.update(cx, |workspace, cx| workspace.open_path(file, true, cx))
|
||||
.await
|
||||
.expect("Could not open test file");
|
||||
|
||||
let editor = cx.update(|cx| {
|
||||
item.act_as::<Editor>(cx)
|
||||
.expect("Opened test file wasn't an editor")
|
||||
});
|
||||
editor.update(cx, |_, cx| cx.focus_self());
|
||||
|
||||
let lsp = fake_servers.next().await.unwrap();
|
||||
|
||||
Self {
|
||||
cx: EditorTestContext {
|
||||
cx,
|
||||
window_id,
|
||||
editor,
|
||||
},
|
||||
lsp,
|
||||
workspace,
|
||||
buffer_lsp_url: lsp::Url::from_file_path("/root/dir/file.rs").unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn new_rust(
|
||||
capabilities: lsp::ServerCapabilities,
|
||||
cx: &'a mut gpui::TestAppContext,
|
||||
) -> EditorLspTestContext<'a> {
|
||||
let language = Language::new(
|
||||
LanguageConfig {
|
||||
name: "Rust".into(),
|
||||
path_suffixes: vec!["rs".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_rust::language()),
|
||||
);
|
||||
|
||||
Self::new(language, capabilities, cx).await
|
||||
}
|
||||
|
||||
// Constructs lsp range using a marked string with '[', ']' range delimiters
|
||||
pub fn lsp_range(&mut self, marked_text: &str) -> lsp::Range {
|
||||
let ranges = self.ranges(marked_text);
|
||||
self.to_lsp_range(ranges[0].clone())
|
||||
}
|
||||
|
||||
pub fn to_lsp_range(&mut self, range: Range<usize>) -> lsp::Range {
|
||||
let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx));
|
||||
let start_point = range.start.to_point(&snapshot.buffer_snapshot);
|
||||
let end_point = range.end.to_point(&snapshot.buffer_snapshot);
|
||||
|
||||
self.editor(|editor, cx| {
|
||||
let buffer = editor.buffer().read(cx);
|
||||
let start = point_to_lsp(
|
||||
buffer
|
||||
.point_to_buffer_offset(start_point, cx)
|
||||
.unwrap()
|
||||
.1
|
||||
.to_point_utf16(&buffer.read(cx)),
|
||||
);
|
||||
let end = point_to_lsp(
|
||||
buffer
|
||||
.point_to_buffer_offset(end_point, cx)
|
||||
.unwrap()
|
||||
.1
|
||||
.to_point_utf16(&buffer.read(cx)),
|
||||
);
|
||||
|
||||
lsp::Range { start, end }
|
||||
})
|
||||
}
|
||||
|
||||
pub fn to_lsp(&mut self, offset: usize) -> lsp::Position {
|
||||
let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx));
|
||||
let point = offset.to_point(&snapshot.buffer_snapshot);
|
||||
|
||||
self.editor(|editor, cx| {
|
||||
let buffer = editor.buffer().read(cx);
|
||||
point_to_lsp(
|
||||
buffer
|
||||
.point_to_buffer_offset(point, cx)
|
||||
.unwrap()
|
||||
.1
|
||||
.to_point_utf16(&buffer.read(cx)),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn update_workspace<F, T>(&mut self, update: F) -> T
|
||||
where
|
||||
F: FnOnce(&mut Workspace, &mut ViewContext<Workspace>) -> T,
|
||||
{
|
||||
self.workspace.update(self.cx.cx, update)
|
||||
}
|
||||
|
||||
pub fn handle_request<T, F, Fut>(
|
||||
&self,
|
||||
mut handler: F,
|
||||
) -> futures::channel::mpsc::UnboundedReceiver<()>
|
||||
where
|
||||
T: 'static + request::Request,
|
||||
T::Params: 'static + Send,
|
||||
F: 'static + Send + FnMut(lsp::Url, T::Params, gpui::AsyncAppContext) -> Fut,
|
||||
Fut: 'static + Send + Future<Output = Result<T::Result>>,
|
||||
{
|
||||
let url = self.buffer_lsp_url.clone();
|
||||
self.lsp.handle_request::<T, _, _>(move |params, cx| {
|
||||
let url = url.clone();
|
||||
handler(url, params, cx)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn notify<T: notification::Notification>(&self, params: T::Params) {
|
||||
self.lsp.notify::<T>(params);
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Deref for EditorLspTestContext<'a> {
|
||||
type Target = EditorTestContext<'a>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.cx
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> DerefMut for EditorLspTestContext<'a> {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.cx
|
||||
}
|
||||
}
|
||||
|
|
208
crates/editor/src/test/editor_lsp_test_context.rs
Normal file
208
crates/editor/src/test/editor_lsp_test_context.rs
Normal file
|
@ -0,0 +1,208 @@
|
|||
use std::{
|
||||
ops::{Deref, DerefMut, Range},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use futures::Future;
|
||||
use gpui::{json, ViewContext, ViewHandle};
|
||||
use language::{point_to_lsp, FakeLspAdapter, Language, LanguageConfig};
|
||||
use lsp::{notification, request};
|
||||
use project::Project;
|
||||
use smol::stream::StreamExt;
|
||||
use workspace::{pane, AppState, Workspace, WorkspaceHandle};
|
||||
|
||||
use crate::{multi_buffer::ToPointUtf16, Editor, ToPoint};
|
||||
|
||||
use super::editor_test_context::EditorTestContext;
|
||||
|
||||
pub struct EditorLspTestContext<'a> {
|
||||
pub cx: EditorTestContext<'a>,
|
||||
pub lsp: lsp::FakeLanguageServer,
|
||||
pub workspace: ViewHandle<Workspace>,
|
||||
pub buffer_lsp_url: lsp::Url,
|
||||
}
|
||||
|
||||
impl<'a> EditorLspTestContext<'a> {
|
||||
pub async fn new(
|
||||
mut language: Language,
|
||||
capabilities: lsp::ServerCapabilities,
|
||||
cx: &'a mut gpui::TestAppContext,
|
||||
) -> EditorLspTestContext<'a> {
|
||||
use json::json;
|
||||
|
||||
cx.update(|cx| {
|
||||
crate::init(cx);
|
||||
pane::init(cx);
|
||||
});
|
||||
|
||||
let params = cx.update(AppState::test);
|
||||
|
||||
let file_name = format!(
|
||||
"file.{}",
|
||||
language
|
||||
.path_suffixes()
|
||||
.first()
|
||||
.unwrap_or(&"txt".to_string())
|
||||
);
|
||||
|
||||
let mut fake_servers = language
|
||||
.set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
|
||||
capabilities,
|
||||
..Default::default()
|
||||
}))
|
||||
.await;
|
||||
|
||||
let project = Project::test(params.fs.clone(), [], cx).await;
|
||||
project.update(cx, |project, _| project.languages().add(Arc::new(language)));
|
||||
|
||||
params
|
||||
.fs
|
||||
.as_fake()
|
||||
.insert_tree("/root", json!({ "dir": { file_name: "" }}))
|
||||
.await;
|
||||
|
||||
let (window_id, workspace) =
|
||||
cx.add_window(|cx| Workspace::new(project.clone(), |_, _| unimplemented!(), cx));
|
||||
project
|
||||
.update(cx, |project, cx| {
|
||||
project.find_or_create_local_worktree("/root", true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx))
|
||||
.await;
|
||||
|
||||
let file = cx.read(|cx| workspace.file_project_paths(cx)[0].clone());
|
||||
let item = workspace
|
||||
.update(cx, |workspace, cx| workspace.open_path(file, true, cx))
|
||||
.await
|
||||
.expect("Could not open test file");
|
||||
|
||||
let editor = cx.update(|cx| {
|
||||
item.act_as::<Editor>(cx)
|
||||
.expect("Opened test file wasn't an editor")
|
||||
});
|
||||
editor.update(cx, |_, cx| cx.focus_self());
|
||||
|
||||
let lsp = fake_servers.next().await.unwrap();
|
||||
|
||||
Self {
|
||||
cx: EditorTestContext {
|
||||
cx,
|
||||
window_id,
|
||||
editor,
|
||||
},
|
||||
lsp,
|
||||
workspace,
|
||||
buffer_lsp_url: lsp::Url::from_file_path("/root/dir/file.rs").unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn new_rust(
|
||||
capabilities: lsp::ServerCapabilities,
|
||||
cx: &'a mut gpui::TestAppContext,
|
||||
) -> EditorLspTestContext<'a> {
|
||||
let language = Language::new(
|
||||
LanguageConfig {
|
||||
name: "Rust".into(),
|
||||
path_suffixes: vec!["rs".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_rust::language()),
|
||||
);
|
||||
|
||||
Self::new(language, capabilities, cx).await
|
||||
}
|
||||
|
||||
// Constructs lsp range using a marked string with '[', ']' range delimiters
|
||||
pub fn lsp_range(&mut self, marked_text: &str) -> lsp::Range {
|
||||
let ranges = self.ranges(marked_text);
|
||||
self.to_lsp_range(ranges[0].clone())
|
||||
}
|
||||
|
||||
pub fn to_lsp_range(&mut self, range: Range<usize>) -> lsp::Range {
|
||||
let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx));
|
||||
let start_point = range.start.to_point(&snapshot.buffer_snapshot);
|
||||
let end_point = range.end.to_point(&snapshot.buffer_snapshot);
|
||||
|
||||
self.editor(|editor, cx| {
|
||||
let buffer = editor.buffer().read(cx);
|
||||
let start = point_to_lsp(
|
||||
buffer
|
||||
.point_to_buffer_offset(start_point, cx)
|
||||
.unwrap()
|
||||
.1
|
||||
.to_point_utf16(&buffer.read(cx)),
|
||||
);
|
||||
let end = point_to_lsp(
|
||||
buffer
|
||||
.point_to_buffer_offset(end_point, cx)
|
||||
.unwrap()
|
||||
.1
|
||||
.to_point_utf16(&buffer.read(cx)),
|
||||
);
|
||||
|
||||
lsp::Range { start, end }
|
||||
})
|
||||
}
|
||||
|
||||
pub fn to_lsp(&mut self, offset: usize) -> lsp::Position {
|
||||
let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx));
|
||||
let point = offset.to_point(&snapshot.buffer_snapshot);
|
||||
|
||||
self.editor(|editor, cx| {
|
||||
let buffer = editor.buffer().read(cx);
|
||||
point_to_lsp(
|
||||
buffer
|
||||
.point_to_buffer_offset(point, cx)
|
||||
.unwrap()
|
||||
.1
|
||||
.to_point_utf16(&buffer.read(cx)),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn update_workspace<F, T>(&mut self, update: F) -> T
|
||||
where
|
||||
F: FnOnce(&mut Workspace, &mut ViewContext<Workspace>) -> T,
|
||||
{
|
||||
self.workspace.update(self.cx.cx, update)
|
||||
}
|
||||
|
||||
pub fn handle_request<T, F, Fut>(
|
||||
&self,
|
||||
mut handler: F,
|
||||
) -> futures::channel::mpsc::UnboundedReceiver<()>
|
||||
where
|
||||
T: 'static + request::Request,
|
||||
T::Params: 'static + Send,
|
||||
F: 'static + Send + FnMut(lsp::Url, T::Params, gpui::AsyncAppContext) -> Fut,
|
||||
Fut: 'static + Send + Future<Output = Result<T::Result>>,
|
||||
{
|
||||
let url = self.buffer_lsp_url.clone();
|
||||
self.lsp.handle_request::<T, _, _>(move |params, cx| {
|
||||
let url = url.clone();
|
||||
handler(url, params, cx)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn notify<T: notification::Notification>(&self, params: T::Params) {
|
||||
self.lsp.notify::<T>(params);
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Deref for EditorLspTestContext<'a> {
|
||||
type Target = EditorTestContext<'a>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.cx
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> DerefMut for EditorLspTestContext<'a> {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.cx
|
||||
}
|
||||
}
|
273
crates/editor/src/test/editor_test_context.rs
Normal file
273
crates/editor/src/test/editor_test_context.rs
Normal file
|
@ -0,0 +1,273 @@
|
|||
use std::{
|
||||
any::TypeId,
|
||||
ops::{Deref, DerefMut, Range},
|
||||
};
|
||||
|
||||
use futures::Future;
|
||||
use indoc::indoc;
|
||||
|
||||
use crate::{
|
||||
display_map::ToDisplayPoint, AnchorRangeExt, Autoscroll, DisplayPoint, Editor, MultiBuffer,
|
||||
};
|
||||
use gpui::{keymap::Keystroke, AppContext, ContextHandle, ModelContext, ViewContext, ViewHandle};
|
||||
use language::{Buffer, BufferSnapshot};
|
||||
use settings::Settings;
|
||||
use util::{
|
||||
assert_set_eq,
|
||||
test::{generate_marked_text, marked_text_ranges},
|
||||
};
|
||||
|
||||
use super::build_editor;
|
||||
|
||||
pub struct EditorTestContext<'a> {
|
||||
pub cx: &'a mut gpui::TestAppContext,
|
||||
pub window_id: usize,
|
||||
pub editor: ViewHandle<Editor>,
|
||||
}
|
||||
|
||||
impl<'a> EditorTestContext<'a> {
|
||||
pub fn new(cx: &'a mut gpui::TestAppContext) -> EditorTestContext<'a> {
|
||||
let (window_id, editor) = cx.update(|cx| {
|
||||
cx.set_global(Settings::test(cx));
|
||||
crate::init(cx);
|
||||
|
||||
let (window_id, editor) = cx.add_window(Default::default(), |cx| {
|
||||
build_editor(MultiBuffer::build_simple("", cx), cx)
|
||||
});
|
||||
|
||||
editor.update(cx, |_, cx| cx.focus_self());
|
||||
|
||||
(window_id, editor)
|
||||
});
|
||||
|
||||
Self {
|
||||
cx,
|
||||
window_id,
|
||||
editor,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn condition(
|
||||
&self,
|
||||
predicate: impl FnMut(&Editor, &AppContext) -> bool,
|
||||
) -> impl Future<Output = ()> {
|
||||
self.editor.condition(self.cx, predicate)
|
||||
}
|
||||
|
||||
pub fn editor<F, T>(&self, read: F) -> T
|
||||
where
|
||||
F: FnOnce(&Editor, &AppContext) -> T,
|
||||
{
|
||||
self.editor.read_with(self.cx, read)
|
||||
}
|
||||
|
||||
pub fn update_editor<F, T>(&mut self, update: F) -> T
|
||||
where
|
||||
F: FnOnce(&mut Editor, &mut ViewContext<Editor>) -> T,
|
||||
{
|
||||
self.editor.update(self.cx, update)
|
||||
}
|
||||
|
||||
pub fn multibuffer<F, T>(&self, read: F) -> T
|
||||
where
|
||||
F: FnOnce(&MultiBuffer, &AppContext) -> T,
|
||||
{
|
||||
self.editor(|editor, cx| read(editor.buffer().read(cx), cx))
|
||||
}
|
||||
|
||||
pub fn update_multibuffer<F, T>(&mut self, update: F) -> T
|
||||
where
|
||||
F: FnOnce(&mut MultiBuffer, &mut ModelContext<MultiBuffer>) -> T,
|
||||
{
|
||||
self.update_editor(|editor, cx| editor.buffer().update(cx, update))
|
||||
}
|
||||
|
||||
pub fn buffer_text(&self) -> String {
|
||||
self.multibuffer(|buffer, cx| buffer.snapshot(cx).text())
|
||||
}
|
||||
|
||||
pub fn buffer<F, T>(&self, read: F) -> T
|
||||
where
|
||||
F: FnOnce(&Buffer, &AppContext) -> T,
|
||||
{
|
||||
self.multibuffer(|multibuffer, cx| {
|
||||
let buffer = multibuffer.as_singleton().unwrap().read(cx);
|
||||
read(buffer, cx)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn update_buffer<F, T>(&mut self, update: F) -> T
|
||||
where
|
||||
F: FnOnce(&mut Buffer, &mut ModelContext<Buffer>) -> T,
|
||||
{
|
||||
self.update_multibuffer(|multibuffer, cx| {
|
||||
let buffer = multibuffer.as_singleton().unwrap();
|
||||
buffer.update(cx, update)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn buffer_snapshot(&self) -> BufferSnapshot {
|
||||
self.buffer(|buffer, _| buffer.snapshot())
|
||||
}
|
||||
|
||||
pub fn simulate_keystroke(&mut self, keystroke_text: &str) -> ContextHandle {
|
||||
let keystroke_under_test_handle =
|
||||
self.add_assertion_context(format!("Simulated Keystroke: {:?}", keystroke_text));
|
||||
let keystroke = Keystroke::parse(keystroke_text).unwrap();
|
||||
self.cx.dispatch_keystroke(self.window_id, keystroke, false);
|
||||
keystroke_under_test_handle
|
||||
}
|
||||
|
||||
pub fn simulate_keystrokes<const COUNT: usize>(
|
||||
&mut self,
|
||||
keystroke_texts: [&str; COUNT],
|
||||
) -> ContextHandle {
|
||||
let keystrokes_under_test_handle =
|
||||
self.add_assertion_context(format!("Simulated Keystrokes: {:?}", keystroke_texts));
|
||||
for keystroke_text in keystroke_texts.into_iter() {
|
||||
self.simulate_keystroke(keystroke_text);
|
||||
}
|
||||
keystrokes_under_test_handle
|
||||
}
|
||||
|
||||
pub fn ranges(&self, marked_text: &str) -> Vec<Range<usize>> {
|
||||
let (unmarked_text, ranges) = marked_text_ranges(marked_text, false);
|
||||
assert_eq!(self.buffer_text(), unmarked_text);
|
||||
ranges
|
||||
}
|
||||
|
||||
pub fn display_point(&mut self, marked_text: &str) -> DisplayPoint {
|
||||
let ranges = self.ranges(marked_text);
|
||||
let snapshot = self
|
||||
.editor
|
||||
.update(self.cx, |editor, cx| editor.snapshot(cx));
|
||||
ranges[0].start.to_display_point(&snapshot)
|
||||
}
|
||||
|
||||
// Returns anchors for the current buffer using `«` and `»`
|
||||
pub fn text_anchor_range(&self, marked_text: &str) -> Range<language::Anchor> {
|
||||
let ranges = self.ranges(marked_text);
|
||||
let snapshot = self.buffer_snapshot();
|
||||
snapshot.anchor_before(ranges[0].start)..snapshot.anchor_after(ranges[0].end)
|
||||
}
|
||||
|
||||
/// Change the editor's text and selections using a string containing
|
||||
/// embedded range markers that represent the ranges and directions of
|
||||
/// each selection.
|
||||
///
|
||||
/// See the `util::test::marked_text_ranges` function for more information.
|
||||
pub fn set_state(&mut self, marked_text: &str) -> ContextHandle {
|
||||
let _state_context = self.add_assertion_context(format!(
|
||||
"Editor State: \"{}\"",
|
||||
marked_text.escape_debug().to_string()
|
||||
));
|
||||
let (unmarked_text, selection_ranges) = marked_text_ranges(marked_text, true);
|
||||
self.editor.update(self.cx, |editor, cx| {
|
||||
editor.set_text(unmarked_text, cx);
|
||||
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
s.select_ranges(selection_ranges)
|
||||
})
|
||||
});
|
||||
_state_context
|
||||
}
|
||||
|
||||
/// Make an assertion about the editor's text and the ranges and directions
|
||||
/// of its selections using a string containing embedded range markers.
|
||||
///
|
||||
/// See the `util::test::marked_text_ranges` function for more information.
|
||||
pub fn assert_editor_state(&mut self, marked_text: &str) {
|
||||
let (unmarked_text, expected_selections) = marked_text_ranges(marked_text, true);
|
||||
let buffer_text = self.buffer_text();
|
||||
assert_eq!(
|
||||
buffer_text, unmarked_text,
|
||||
"Unmarked text doesn't match buffer text"
|
||||
);
|
||||
self.assert_selections(expected_selections, marked_text.to_string())
|
||||
}
|
||||
|
||||
pub fn assert_editor_background_highlights<Tag: 'static>(&mut self, marked_text: &str) {
|
||||
let expected_ranges = self.ranges(marked_text);
|
||||
let actual_ranges: Vec<Range<usize>> = self.update_editor(|editor, cx| {
|
||||
let snapshot = editor.snapshot(cx);
|
||||
editor
|
||||
.background_highlights
|
||||
.get(&TypeId::of::<Tag>())
|
||||
.map(|h| h.1.clone())
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(|range| range.to_offset(&snapshot.buffer_snapshot))
|
||||
.collect()
|
||||
});
|
||||
assert_set_eq!(actual_ranges, expected_ranges);
|
||||
}
|
||||
|
||||
pub fn assert_editor_text_highlights<Tag: ?Sized + 'static>(&mut self, marked_text: &str) {
|
||||
let expected_ranges = self.ranges(marked_text);
|
||||
let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx));
|
||||
let actual_ranges: Vec<Range<usize>> = snapshot
|
||||
.highlight_ranges::<Tag>()
|
||||
.map(|ranges| ranges.as_ref().clone().1)
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(|range| range.to_offset(&snapshot.buffer_snapshot))
|
||||
.collect();
|
||||
assert_set_eq!(actual_ranges, expected_ranges);
|
||||
}
|
||||
|
||||
pub fn assert_editor_selections(&mut self, expected_selections: Vec<Range<usize>>) {
|
||||
let expected_marked_text =
|
||||
generate_marked_text(&self.buffer_text(), &expected_selections, true);
|
||||
self.assert_selections(expected_selections, expected_marked_text)
|
||||
}
|
||||
|
||||
fn assert_selections(
|
||||
&mut self,
|
||||
expected_selections: Vec<Range<usize>>,
|
||||
expected_marked_text: String,
|
||||
) {
|
||||
let actual_selections = self
|
||||
.editor
|
||||
.read_with(self.cx, |editor, cx| editor.selections.all::<usize>(cx))
|
||||
.into_iter()
|
||||
.map(|s| {
|
||||
if s.reversed {
|
||||
s.end..s.start
|
||||
} else {
|
||||
s.start..s.end
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let actual_marked_text =
|
||||
generate_marked_text(&self.buffer_text(), &actual_selections, true);
|
||||
if expected_selections != actual_selections {
|
||||
panic!(
|
||||
indoc! {"
|
||||
{}Editor has unexpected selections.
|
||||
|
||||
Expected selections:
|
||||
{}
|
||||
|
||||
Actual selections:
|
||||
{}
|
||||
"},
|
||||
self.assertion_context(),
|
||||
expected_marked_text,
|
||||
actual_marked_text,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Deref for EditorTestContext<'a> {
|
||||
type Target = gpui::TestAppContext;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.cx
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> DerefMut for EditorTestContext<'a> {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.cx
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue