editor: typewriter room as rows, so drawing and text queries share one origin; a placement resets centering
This commit is contained in:
8 files changed
+91
-19
No files matched your search
@@ -106,7 +106,7 @@ impl MarkdownDocument {
|
|||||||
if matches!(self.mode, MarkdownMode::Source | MarkdownMode::Split) {
|
if matches!(self.mode, MarkdownMode::Source | MarkdownMode::Split) {
|
||||||
editor
|
editor
|
||||||
.scroll
|
.scroll
|
||||||
.scroll_to_item_strict(line, ScrollStrategy::Top);
|
.scroll_to_item_strict(line + editor.pad, ScrollStrategy::Top);
|
||||||
self.placing(ask);
|
self.placing(ask);
|
||||||
// The editor's offset holds it from the next frame.
|
// The editor's offset holds it from the next frame.
|
||||||
return cx.on_next_frame(window, move |document, _, cx| {
|
return cx.on_next_frame(window, move |document, _, cx| {
|
||||||
@@ -158,6 +158,8 @@ impl MarkdownDocument {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
self.top = None;
|
self.top = None;
|
||||||
|
// The view moved: center again.
|
||||||
|
self.centered = None;
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -216,3 +216,67 @@ fn the_last_source_line_centers_and_a_resize_follows(cx: &mut TestAppContext) {
|
|||||||
"top {top} to {resized}; mid-view is {middle}"
|
"top {top} to {resized}; mid-view is {middle}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Typewriter source, caret on line 60 of 120.
|
||||||
|
fn centered_source(
|
||||||
|
cx: &mut TestAppContext,
|
||||||
|
) -> (
|
||||||
|
gpui::Entity<super::MarkdownDocument>,
|
||||||
|
&mut gpui::VisualTestContext,
|
||||||
|
) {
|
||||||
|
let text: String = (0..120).map(|n| format!("Line {n}.\n")).collect();
|
||||||
|
let (document, cx) = open(&text, MarkdownMode::Source, cx);
|
||||||
|
document.update(cx, |document, cx| {
|
||||||
|
document.set_look(typewriter(), cx);
|
||||||
|
document.set_caret(60, 0, cx);
|
||||||
|
});
|
||||||
|
frames(cx);
|
||||||
|
(document, cx)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
fn source_lines_below_the_center_draw(cx: &mut TestAppContext) {
|
||||||
|
let (_, cx) = centered_source(cx);
|
||||||
|
let caret = cx.debug_bounds("editor-line-60").expect("caret line");
|
||||||
|
let next = cx.debug_bounds("editor-line-61").expect("the line below");
|
||||||
|
assert!(next.top() >= caret.bottom() - px(1.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
fn platform_text_queries_meet_the_drawn_line(cx: &mut TestAppContext) {
|
||||||
|
use gpui::EntityInputHandler as _;
|
||||||
|
let (document, cx) = centered_source(cx);
|
||||||
|
let drawn = cx.debug_bounds("editor-line-60").expect("caret line");
|
||||||
|
let editor = document.read_with(cx, |document, _| document.editor().clone());
|
||||||
|
let (anchor, line) = editor.update_in(cx, |editor, window, cx| {
|
||||||
|
let at = editor.text()[..editor.primary().head]
|
||||||
|
.encode_utf16()
|
||||||
|
.count();
|
||||||
|
let anchor = editor.bounds_for_range(at..at, gpui::Bounds::default(), window, cx);
|
||||||
|
let point = gpui::point(editor.metrics.left, drawn.center().y);
|
||||||
|
let index = editor.character_index_for_point(point, window, cx);
|
||||||
|
let line = index.map(|at| editor.buffer.line_of(editor.byte_range(&(at..at)).start));
|
||||||
|
(anchor.expect("anchor"), line)
|
||||||
|
});
|
||||||
|
assert!(
|
||||||
|
(anchor.top() - drawn.top()).abs() < px(2.0),
|
||||||
|
"{anchor:?} vs {drawn:?}"
|
||||||
|
);
|
||||||
|
assert_eq!(line, Some(60));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
fn switching_to_visual_centers_the_caret_block(cx: &mut TestAppContext) {
|
||||||
|
let (document, cx) = open(&long(), MarkdownMode::Source, cx);
|
||||||
|
document.update(cx, |document, cx| {
|
||||||
|
document.set_look(typewriter(), cx);
|
||||||
|
document.set_caret(100, 0, cx);
|
||||||
|
});
|
||||||
|
frames(cx);
|
||||||
|
document.update_in(cx, |document, window, cx| {
|
||||||
|
document.set_mode(MarkdownMode::Visual, window, cx)
|
||||||
|
});
|
||||||
|
frames(cx);
|
||||||
|
let off = off_middle(&document, 50, cx);
|
||||||
|
assert!(off < px(4.0), "{off:?} from the middle");
|
||||||
|
}
|
||||||
+1
-1
@@ -104,7 +104,7 @@ impl CodeEditor {
|
|||||||
to_utf16(text, range.start)..to_utf16(text, range.end)
|
to_utf16(text, range.start)..to_utf16(text, range.end)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn byte_range(&self, range: &Range<usize>) -> Range<usize> {
|
pub(crate) fn byte_range(&self, range: &Range<usize>) -> Range<usize> {
|
||||||
let text = self.buffer.text();
|
let text = self.buffer.text();
|
||||||
from_utf16(text, range.start)..from_utf16(text, range.end)
|
from_utf16(text, range.start)..from_utf16(text, range.end)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ pub(crate) enum Row {
|
|||||||
Ghost(usize),
|
Ghost(usize),
|
||||||
/// The inline chat's rows; the first draws it.
|
/// The inline chat's rows; the first draws it.
|
||||||
Chat(usize),
|
Chat(usize),
|
||||||
|
/// Typewriter room past an end.
|
||||||
|
Room,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Which lines folds hide.
|
/// Which lines folds hide.
|
||||||
|
|||||||
@@ -158,6 +158,7 @@ impl CodeEditor {
|
|||||||
.iter()
|
.iter()
|
||||||
.rposition(|row| matches!(row, Row::Line(shown) if *shown <= line))
|
.rposition(|row| matches!(row, Row::Line(shown) if *shown <= line))
|
||||||
.unwrap_or(0)
|
.unwrap_or(0)
|
||||||
|
+ self.pad
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Scrolls the primary cursor into view: from below when moving down, from above when moving up.
|
/// Scrolls the primary cursor into view: from below when moving down, from above when moving up.
|
||||||
|
|||||||
@@ -23,6 +23,10 @@ impl CodeEditor {
|
|||||||
.h(self.frame.line)
|
.h(self.frame.line)
|
||||||
.flex();
|
.flex();
|
||||||
let caret = self.buffer.line_of(self.primary().head);
|
let caret = self.buffer.line_of(self.primary().head);
|
||||||
|
let base = match self.frame.rows[ix] {
|
||||||
|
Row::Line(line) => base.debug_selector(move || format!("editor-line-{line}")),
|
||||||
|
_ => base,
|
||||||
|
};
|
||||||
match self.frame.rows[ix] {
|
match self.frame.rows[ix] {
|
||||||
Row::Line(line) if self.options.focus_line && line != caret => {
|
Row::Line(line) if self.options.focus_line && line != caret => {
|
||||||
self.line_row(base.opacity(FADED), ix, line, window, cx)
|
self.line_row(base.opacity(FADED), ix, line, window, cx)
|
||||||
@@ -32,6 +36,7 @@ impl CodeEditor {
|
|||||||
Row::Removed(hunk, at) => self.removed_row(base, ix, hunk, at, cx),
|
Row::Removed(hunk, at) => self.removed_row(base, ix, hunk, at, cx),
|
||||||
Row::Ghost(at) => self.ghost_row(base, ix, at, cx),
|
Row::Ghost(at) => self.ghost_row(base, ix, at, cx),
|
||||||
Row::Chat(at) => self.chat_row(base, ix, at, cx),
|
Row::Chat(at) => self.chat_row(base, ix, at, cx),
|
||||||
|
Row::Room => base.into_any_element(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+4
-4
@@ -100,7 +100,7 @@ pub(crate) struct Metrics {
|
|||||||
pub top: Pixels,
|
pub top: Pixels,
|
||||||
pub advance: Pixels,
|
pub advance: Pixels,
|
||||||
pub line: Pixels,
|
pub line: Pixels,
|
||||||
/// The view's height, under any typewriter room.
|
/// The view's height.
|
||||||
pub height: Pixels,
|
pub height: Pixels,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,8 +124,8 @@ pub struct CodeEditor {
|
|||||||
pub(crate) metrics: Metrics,
|
pub(crate) metrics: Metrics,
|
||||||
/// The caret row typewriter scrolling last centered.
|
/// The caret row typewriter scrolling last centered.
|
||||||
pub(crate) centered: Option<usize>,
|
pub(crate) centered: Option<usize>,
|
||||||
/// Typewriter room past each end, as last drawn.
|
/// Typewriter room rows at each end.
|
||||||
pub(crate) room: Pixels,
|
pub(crate) pad: usize,
|
||||||
pub(crate) dragging: bool,
|
pub(crate) dragging: bool,
|
||||||
pub(crate) caret_on: bool,
|
pub(crate) caret_on: bool,
|
||||||
/// Blink runs only while focused, or unseen cursors redraw forever.
|
/// Blink runs only while focused, or unseen cursors redraw forever.
|
||||||
@@ -212,7 +212,7 @@ impl CodeEditor {
|
|||||||
composing: None,
|
composing: None,
|
||||||
metrics: Metrics::default(),
|
metrics: Metrics::default(),
|
||||||
centered: None,
|
centered: None,
|
||||||
room: Pixels::ZERO,
|
pad: 0,
|
||||||
dragging: false,
|
dragging: false,
|
||||||
caret_on: true,
|
caret_on: true,
|
||||||
focused: false,
|
focused: false,
|
||||||
|
|||||||
+11
-13
@@ -54,8 +54,7 @@ impl CodeEditor {
|
|||||||
|
|
||||||
/// The first line the list shows, after scrolling.
|
/// The first line the list shows, after scrolling.
|
||||||
pub(crate) fn top_line(&self) -> Option<usize> {
|
pub(crate) fn top_line(&self) -> Option<usize> {
|
||||||
// Typewriter room above the first line is no line.
|
let scrolled = -self.scroll.0.borrow().base_handle.offset().y;
|
||||||
let scrolled = -self.scroll.0.borrow().base_handle.offset().y - self.room;
|
|
||||||
let first = (scrolled / self.frame.line).floor().max(0.0) as usize;
|
let first = (scrolled / self.frame.line).floor().max(0.0) as usize;
|
||||||
self.frame.rows[first.min(self.frame.rows.len().saturating_sub(1))..]
|
self.frame.rows[first.min(self.frame.rows.len().saturating_sub(1))..]
|
||||||
.iter()
|
.iter()
|
||||||
@@ -252,8 +251,16 @@ impl Render for CodeEditor {
|
|||||||
_ => Vec::new(),
|
_ => Vec::new(),
|
||||||
};
|
};
|
||||||
let minimap = self.frame.minimap;
|
let minimap = self.frame.minimap;
|
||||||
|
// Half a view of room rows: the ends center too.
|
||||||
|
self.pad = match self.options.typewriter && line > Pixels::ZERO {
|
||||||
|
true => (self.metrics.height / 2.0 / line).ceil() as usize,
|
||||||
|
false => 0,
|
||||||
|
};
|
||||||
|
let mut shown = vec![Row::Room; self.pad];
|
||||||
|
shown.extend(rows(&self.buffer, &hide, &self.marks));
|
||||||
|
shown.extend(std::iter::repeat_n(Row::Room, self.pad));
|
||||||
self.frame = Frame {
|
self.frame = Frame {
|
||||||
rows: rows(&self.buffer, &hide, &self.marks),
|
rows: shown,
|
||||||
folds: found,
|
folds: found,
|
||||||
brackets: found_brackets,
|
brackets: found_brackets,
|
||||||
advance,
|
advance,
|
||||||
@@ -271,13 +278,6 @@ impl Render for CodeEditor {
|
|||||||
.scroll_to_item_strict(row, ScrollStrategy::Center);
|
.scroll_to_item_strict(row, ScrollStrategy::Center);
|
||||||
self.centered = Some(row);
|
self.centered = Some(row);
|
||||||
}
|
}
|
||||||
// Half a view of room: a resize keeps the center.
|
|
||||||
let height = self.metrics.height;
|
|
||||||
self.room = match self.options.typewriter {
|
|
||||||
true => height / 2.0,
|
|
||||||
false => gpui::px(0.0),
|
|
||||||
};
|
|
||||||
let room = self.room;
|
|
||||||
let sticky = if self.options.sticky {
|
let sticky = if self.options.sticky {
|
||||||
self.sticky(cx)
|
self.sticky(cx)
|
||||||
} else {
|
} else {
|
||||||
@@ -292,9 +292,7 @@ impl Render for CodeEditor {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.track_scroll(&self.scroll)
|
.track_scroll(&self.scroll)
|
||||||
.size_full()
|
.size_full();
|
||||||
.pt(room)
|
|
||||||
.pb(room);
|
|
||||||
let (top, focus, input) = (cx.entity(), self.focus.clone(), cx.entity());
|
let (top, focus, input) = (cx.entity(), self.focus.clone(), cx.entity());
|
||||||
let unlock = cx.entity();
|
let unlock = cx.entity();
|
||||||
let banner = self.options.read_only.then(|| {
|
let banner = self.options.read_only.then(|| {
|
||||||
|
|||||||
Reference in new issue
Block a user