Panics · P17 Editor: owner offsets ride the text's edits, stale ranges, rows and globs log instead of panicking

This commit is contained in:
ZacharyZhang-NY committed 2026-10-06 21:13:15 -04:00
1 parent f0b7f779f6
commit b29d66ccc0
13 files changed
+489 -83

No files matched your search

+83
View File
@@ -0,0 +1,83 @@
use std::ops::Range;
use super::state::Marks;
/// Where `offset` lands after `edits`, sorted and apart, each a replaced range and the length written; none when an edit replaced it.
fn moved(offset: usize, edits: &[(Range<usize>, usize)]) -> Option<usize> {
let mut at = offset as isize;
for (range, written) in edits {
if offset <= range.start {
break;
}
if offset < range.end {
return None;
}
at += *written as isize - range.len() as isize;
}
Some(at as usize)
}
/// A range through `edits`; none when an edit cut into it.
fn moved_range(range: &Range<usize>, edits: &[(Range<usize>, usize)]) -> Option<Range<usize>> {
Some(moved(range.start, edits)?..moved(range.end, edits)?)
}
/// Carries the owner's offsets through an edit of the text, dropping what it replaced.
pub(crate) fn shift(marks: &mut Marks, edits: &[(Range<usize>, usize)]) {
marks.diagnostics.retain_mut(|diagnostic| {
moved_range(&diagnostic.range, edits)
.map(|range| diagnostic.range = range)
.is_some()
});
marks.backgrounds.retain_mut(|(range, _)| {
moved_range(range, edits)
.map(|moved| *range = moved)
.is_some()
});
marks.hints.retain_mut(|hint| {
moved(hint.offset, edits)
.map(|at| hint.offset = at)
.is_some()
});
marks.ghost = marks.ghost.take().and_then(|mut ghost| {
ghost.offset = moved(ghost.offset, edits)?;
Some(ghost)
});
}
/// Drops the owner's offsets when the whole text changes; they named the old one.
pub(crate) fn clear(marks: &mut Marks) {
marks.diagnostics.clear();
marks.backgrounds.clear();
marks.hints.clear();
marks.ghost = None;
}
/// Whether `range` runs forward between whole characters of `text`.
pub(crate) fn fits(text: &str, range: &Range<usize>) -> bool {
range.start <= range.end
&& text.is_char_boundary(range.start)
&& text.is_char_boundary(range.end)
}
#[cfg(test)]
mod tests {
use super::{fits, moved};
#[test]
fn offsets_ride_their_edits() {
let edits = [(2..4, 1), (6..6, 3)];
let cut = 0..2;
assert_eq!(moved(1, &edits), Some(1), "before every edit");
assert_eq!(moved(2, &edits), Some(2), "at an edit's start");
assert_eq!(moved(3, &edits), None, "inside what it replaced");
assert_eq!(moved(5, &edits), Some(4), "past it, by its change");
assert_eq!(
moved(6, &edits),
Some(5),
"at an insertion, before what it adds"
);
assert_eq!(moved(8, &edits), Some(10));
assert!(fits("héllo", &(1..3)) && !fits("héllo", &cut) && !fits("ab", &(1..9)));
}
}
+3 -4
View File
@@ -52,10 +52,9 @@ impl OutputPanel {
) -> Self {
let channels: Vec<SharedString> = channels.into_iter().map(Into::into).collect();
let channel = channel.into();
assert!(
channels.contains(&channel),
"the shown channel is one of the channels"
);
if !channels.contains(&channel) {
log::error!("output panel: {channel} is not among {channels:?}; none chosen");
}
Self {
id: id.into(),
channels,
+72 -1
View File
@@ -1,8 +1,9 @@
use std::ops::Range;
use gpui::{ClipboardItem, Context};
use gpui::{ClipboardItem, Context, ScrollStrategy};
use super::{
anchors,
buffer::Buffer,
cursor::{Motion, Selection, merged, replace_each},
state::CodeEditor,
@@ -39,6 +40,10 @@ impl CodeEditor {
let before = self.snapshot();
let primary = self.primary().range().start;
let edits = joined(edits);
let spans: Vec<(Range<usize>, usize)> = edits
.iter()
.map(|(range, text)| (range.clone(), text.len()))
.collect();
let lead = edits
.iter()
.rposition(|(range, _)| range.start <= primary)
@@ -48,6 +53,7 @@ impl CodeEditor {
carets.push(lead);
self.selections = merged(carets);
self.marked = None;
anchors::shift(&mut self.marks, &spans);
self.commit(before, typing, cx);
self.reveal(true, cx);
}
@@ -424,3 +430,68 @@ fn joined(mut edits: Vec<(Range<usize>, String)>) -> Vec<(Range<usize>, String)>
}
out
}
impl CodeEditor {
/// Selects these ranges, the last one primary.
pub fn select(
&mut self,
ranges: impl IntoIterator<Item = Range<usize>>,
cx: &mut Context<Self>,
) {
let chosen: Vec<Selection> = ranges
.into_iter()
.filter(|range| {
let whole = self.whole(range);
if !whole {
log::error!(
"code editor: no selection {range:?} in {} bytes",
self.buffer.len()
);
}
whole
})
.map(|range| Selection {
anchor: range.start,
head: range.end,
goal: None,
})
.collect();
if chosen.is_empty() {
return;
}
if merged(chosen.clone()) == self.selections {
return;
}
self.set_selections(chosen, cx);
self.scroll
.scroll_to_item(self.primary_row(), ScrollStrategy::Center);
}
/// Replaces ranges of the text at once, as one undo step; read-only text refuses.
pub fn edit(
&mut self,
edits: impl IntoIterator<Item = (Range<usize>, String)>,
cx: &mut Context<Self>,
) {
let edits: Vec<(Range<usize>, String)> = edits.into_iter().collect();
let mut starts: Vec<&Range<usize>> = edits.iter().map(|(range, _)| range).collect();
starts.sort_by_key(|range| range.start);
let apart = starts.windows(2).all(|pair| pair[0].end <= pair[1].start);
let forward = edits.iter().all(|(range, _)| range.start <= range.end);
if !apart || !forward || !edits.iter().all(|(range, _)| self.whole(range)) {
log::error!(
"code editor: edits {:?} do not fit {} bytes; none made",
starts,
self.buffer.len()
);
return;
}
self.apply(edits, false, cx);
}
/// Whether both ends of `range` fall between whole characters of the text.
fn whole(&self, range: &Range<usize>) -> bool {
let text = self.buffer.text();
text.is_char_boundary(range.start) && text.is_char_boundary(range.end)
}
}
+11 -4
View File
@@ -11,7 +11,7 @@ use crate::{
/// Whether a path passes comma-separated globs: some include pattern when there are any, and no exclude one.
pub fn passes(path: &str, include: &str, exclude: &str) -> bool {
let patterns = |list: &str| -> Vec<regex::Regex> {
let patterns = |list: &str| -> Vec<Option<regex::Regex>> {
list.split(',')
.map(str::trim)
.filter(|glob| !glob.is_empty())
@@ -34,13 +34,20 @@ pub fn passes(path: &str, include: &str, exclude: &str) -> bool {
}
}
pattern.push('$');
regex::Regex::new(&pattern).expect("an escaped glob compiles")
regex::Regex::new(&pattern)
.inspect_err(|error| {
log::error!(
"search filter: a glob of {} bytes matches nothing: {error}",
pattern.len()
)
})
.ok()
})
.collect()
};
let (include, exclude) = (patterns(include), patterns(exclude));
(include.is_empty() || include.iter().any(|glob| glob.is_match(path)))
&& !exclude.iter().any(|glob| glob.is_match(path))
(include.is_empty() || include.iter().flatten().any(|glob| glob.is_match(path)))
&& !exclude.iter().flatten().any(|glob| glob.is_match(path))
}
type OnBool = Rc<dyn Fn(bool, &mut Window, &mut App)>;
+7 -4
View File
@@ -82,17 +82,20 @@ impl CodeEditor {
let Row::Line(line) = *row else {
return None;
};
Some(self.offset_in(line, position.x, cx))
self.offset_in(line, position.x, cx)
}
/// The offset nearest window x on a line.
pub(crate) fn offset_in(&self, line: usize, x: Pixels, cx: &App) -> usize {
/// The offset nearest window x on a line; none for a line the text has since lost.
pub(crate) fn offset_in(&self, line: usize, x: Pixels, cx: &App) -> Option<usize> {
if line >= self.buffer.lines() {
return None;
}
let text = self.row_text(line);
let shown = Shown::new(text, self.notes(line, cx), Vec::new());
let column = ((x - self.metrics.left) / self.metrics.advance)
.round()
.max(0.0) as usize;
self.buffer.offset(line, shown.back(text, column))
Some(self.buffer.offset(line, shown.back(text, column)))
}
fn utf16_range(&self, range: &Range<usize>) -> Range<usize> {
+6 -2
View File
@@ -353,7 +353,9 @@ impl CodeEditor {
cx: &mut Context<Self>,
) {
window.focus(&self.focus, cx);
let at = self.offset_in(line, event.position.x, cx);
let Some(at) = self.offset_in(line, event.position.x, cx) else {
return;
};
let chosen = match event.click_count {
2 => {
let word = self.buffer.word_at(at);
@@ -396,7 +398,9 @@ impl CodeEditor {
if !self.dragging || !event.dragging() {
return;
}
let at = self.offset_in(line, event.position.x, cx);
let Some(at) = self.offset_in(line, event.position.x, cx) else {
return;
};
let mut all = self.selections.clone();
let primary = all.last_mut().expect("an editor keeps a cursor");
if primary.head == at {
+1
View File
@@ -1,3 +1,4 @@
mod anchors;
mod buffer;
mod chat;
mod console;
+3 -3
View File
@@ -150,14 +150,14 @@ impl CodeEditor {
});
}
/// The row the primary cursor draws on.
/// The row the primary cursor draws on, or the fold header above it when a fold hides it.
pub(crate) fn primary_row(&self) -> usize {
let line = self.buffer.line_of(self.primary().head);
let hide = hidden(self.buffer.lines(), &folds(&self.buffer), &self.folded);
rows(&self.buffer, &hide, &self.marks)
.iter()
.position(|row| *row == Row::Line(line))
.expect("the primary cursor sits on a shown line")
.rposition(|row| matches!(row, Row::Line(shown) if *shown <= line))
.unwrap_or(0)
}
/// Scrolls the primary cursor into view: from below when moving down, from above when moving up.
+27 -52
View File
@@ -1,11 +1,12 @@
use std::{collections::BTreeSet, ops::Range, time::Duration};
use gpui::{
App, AppContext as _, Context, EventEmitter, FocusHandle, Focusable, Pixels, ScrollStrategy,
SharedString, Subscription, Task, UniformListScrollHandle, Window,
App, AppContext as _, Context, EventEmitter, FocusHandle, Focusable, Pixels, SharedString,
Subscription, Task, UniformListScrollHandle, Window,
};
use super::{
anchors,
buffer::Buffer,
cursor::{Selection, merged},
decor::{CodeLens, Diagnostic, DiffHunk, GhostText, GitMark, InlayHint},
@@ -129,6 +130,14 @@ impl Focusable for CodeEditor {
}
}
/// Whether a mark the owner sent fits the text; one that does not is logged and dropped.
fn kept(fits: bool, what: &str) -> bool {
if !fits {
log::error!("code editor: {what} lies off the text; dropped");
}
fits
}
impl CodeEditor {
pub fn new(text: impl Into<String>, window: &mut Window, cx: &mut Context<Self>) -> Self {
let focus = cx.focus_handle().tab_stop(true);
@@ -291,6 +300,8 @@ impl CodeEditor {
self.buffer = Buffer::new(text);
self.selections = vec![Selection::caret(0)];
self.folded.clear();
(self.marked, self.composing) = (None, None);
anchors::clear(&mut self.marks);
self.commit(before, false, cx);
}
@@ -310,37 +321,10 @@ impl CodeEditor {
cx.notify();
}
/// Selects these ranges, the last one primary.
pub fn select(
&mut self,
ranges: impl IntoIterator<Item = Range<usize>>,
cx: &mut Context<Self>,
) {
let chosen: Vec<Selection> = ranges
.into_iter()
.map(|range| {
assert!(
range.end <= self.buffer.len()
&& self.buffer.text().is_char_boundary(range.start)
&& self.buffer.text().is_char_boundary(range.end),
"a selection covers whole characters of the text"
);
Selection {
anchor: range.start,
head: range.end,
goal: None,
}
})
.collect();
if merged(chosen.clone()) == self.selections {
return;
}
self.set_selections(chosen, cx);
self.scroll
.scroll_to_item(self.primary_row(), ScrollStrategy::Center);
}
pub fn set_diagnostics(&mut self, diagnostics: Vec<Diagnostic>, cx: &mut Context<Self>) {
pub fn set_diagnostics(&mut self, mut diagnostics: Vec<Diagnostic>, cx: &mut Context<Self>) {
let text = self.buffer.text();
diagnostics
.retain(|diagnostic| kept(anchors::fits(text, &diagnostic.range), "a diagnostic"));
if self.marks.diagnostics == diagnostics {
return;
}
@@ -350,9 +334,11 @@ impl CodeEditor {
pub fn set_backgrounds(
&mut self,
ranges: Vec<(Range<usize>, gpui::Hsla)>,
mut ranges: Vec<(Range<usize>, gpui::Hsla)>,
cx: &mut Context<Self>,
) {
let text = self.buffer.text();
ranges.retain(|(range, _)| kept(anchors::fits(text, range), "a background"));
if self.marks.backgrounds == ranges {
return;
}
@@ -360,7 +346,9 @@ impl CodeEditor {
cx.notify();
}
pub fn set_inlay_hints(&mut self, hints: Vec<InlayHint>, cx: &mut Context<Self>) {
pub fn set_inlay_hints(&mut self, mut hints: Vec<InlayHint>, cx: &mut Context<Self>) {
let text = self.buffer.text();
hints.retain(|hint| kept(text.is_char_boundary(hint.offset), "an inlay hint"));
self.marks.hints = hints;
self.marks.hints.sort_by_key(|hint| hint.offset);
cx.notify();
@@ -373,7 +361,9 @@ impl CodeEditor {
/// Suggested text at an offset, in grey until Tab takes it.
pub fn set_ghost_text(&mut self, ghost: Option<GhostText>, cx: &mut Context<Self>) {
self.marks.ghost = ghost;
let text = self.buffer.text();
self.marks.ghost =
ghost.filter(|ghost| kept(text.is_char_boundary(ghost.offset), "ghost text"));
cx.notify();
}
@@ -412,22 +402,6 @@ impl CodeEditor {
cx.notify();
}
/// Replaces ranges of the text at once, as one undo step; read-only text refuses.
pub fn edit(
&mut self,
edits: impl IntoIterator<Item = (Range<usize>, String)>,
cx: &mut Context<Self>,
) {
let edits: Vec<(Range<usize>, String)> = edits.into_iter().collect();
assert!(
edits.iter().all(|(range, _)| range.end <= self.buffer.len()
&& self.buffer.text().is_char_boundary(range.start)
&& self.buffer.text().is_char_boundary(range.end)),
"an edit covers whole characters of the text"
);
self.apply(edits, false, cx);
}
pub(crate) fn set_selections(&mut self, selections: Vec<Selection>, cx: &mut Context<Self>) {
self.selections = merged(selections);
self.unfold_cursors();
@@ -446,6 +420,7 @@ impl CodeEditor {
self.selections = snapshot.selections;
self.marked = None;
self.composing = None;
anchors::clear(&mut self.marks);
cx.emit(EditorEvent::Changed);
self.restart_blink(cx);
}
@@ -1,3 +1,5 @@
mod stale;
use std::{cell::Cell, rc::Rc, time::Duration};
use gpui::{
@@ -8,7 +10,7 @@ use gpui::{
use super::{CodeEditor, GhostText, InlayHint, LineNumbers, layout::Row};
use crate::theme::Theme;
fn editor<'a>(
pub(super) fn editor<'a>(
text: &str,
cx: &'a mut TestAppContext,
) -> (Entity<CodeEditor>, &'a mut VisualTestContext) {
+252
View File
@@ -0,0 +1,252 @@
use gpui::{Entity, EntityInputHandler, TestAppContext};
use super::editor;
use crate::{
editor::{CodeEditor, GhostText, InlayHint},
theme::Theme,
};
#[gpui::test]
fn ranges_from_an_older_text_are_left(cx: &mut TestAppContext) {
let (editor, cx) = editor("héllo", cx);
editor.update(cx, |editor, cx| {
let caret = 1..1;
editor.select([caret], cx);
editor.select([0..2, 0..99], cx);
assert_eq!(
editor.primary().head,
1,
"a stale selection leaves the last"
);
editor.edit([(0..2, "x".to_string())], cx);
editor.edit([(0..3, "x".to_string()), (1..4, "y".to_string())], cx);
editor.edit(
[(std::ops::Range { start: 3, end: 1 }, "x".to_string())],
cx,
);
assert_eq!(
editor.text(),
"héllo",
"a stale, crossing or backward batch makes no edit"
);
editor.edit([(0..1, "j".to_string())], cx);
assert_eq!(editor.text(), "jéllo");
});
}
/// An output panel whose shown channel left its list.
struct Output;
impl gpui::Render for Output {
fn render(
&mut self,
_: &mut gpui::Window,
_: &mut gpui::Context<Self>,
) -> impl gpui::IntoElement {
crate::editor::OutputPanel::new("output", ["Build", "Tests"], "Lint", ["one line"])
}
}
#[gpui::test]
fn an_output_channel_gone_from_its_list_draws(cx: &mut TestAppContext) {
cx.update(Theme::init);
let (_, cx) = cx.add_window_view(|_, _| Output);
cx.run_until_parked();
}
#[test]
fn a_glob_too_big_for_a_regex_matches_nothing() {
let huge = "?".repeat(500_000);
assert!(!crate::editor::passes("src/a.rs", &huge, ""));
assert!(crate::editor::passes("src/a.rs", "", &huge));
}
#[gpui::test]
fn new_text_drops_what_named_the_old(cx: &mut TestAppContext) {
let (editor, cx) = editor("abcd", cx);
editor.update_in(cx, |editor, window, cx| {
let end = 4..4;
editor.select([end], cx);
editor.replace_and_mark_text_in_range(None, "ni", None, window, cx);
editor.set_inlay_hints(
vec![InlayHint {
offset: 1,
text: ": i32".into(),
}],
cx,
);
editor.set_backgrounds(vec![(1..2, gpui::black())], cx);
editor.set_text("é", cx);
editor.replace_text_in_range(None, "你", window, cx);
editor.set_ghost_text(
Some(GhostText {
offset: 4,
text: "late".into(),
}),
cx,
);
editor.indent(cx);
});
cx.run_until_parked();
assert!(editor.read_with(cx, |editor, _| editor.marks.hints.is_empty()));
}
#[gpui::test]
fn a_diagnostic_rides_the_typing_before_it(cx: &mut TestAppContext) {
let (editor, cx) = editor("ab", cx);
editor.update(cx, |editor, cx| {
let note = |range| crate::editor::Diagnostic {
range,
severity: crate::primitives::Severity::Danger,
message: "note".into(),
};
editor.set_diagnostics(vec![note(1..2)], cx);
let start = 0..0;
editor.select([start], cx);
editor.type_text("é", cx);
assert_eq!(editor.marks.diagnostics[0].range, 3..4);
let whole = 0..4;
editor.select([whole], cx);
editor.type_text("x", cx);
assert!(
editor.marks.diagnostics.is_empty(),
"an edit over it takes it"
);
});
cx.run_until_parked();
}
/// A sticky editor in a box short enough to scroll.
struct Boxed(Entity<CodeEditor>);
impl gpui::Render for Boxed {
fn render(
&mut self,
_: &mut gpui::Window,
_: &mut gpui::Context<Self>,
) -> impl gpui::IntoElement {
use gpui::{ParentElement as _, Styled as _};
gpui::div()
.w(gpui::px(480.0))
.h(gpui::px(240.0))
.child(self.0.clone())
}
}
#[gpui::test]
fn a_scrolled_sticky_editor_takes_a_shorter_text(cx: &mut TestAppContext) {
use gpui::AppContext as _;
cx.update(Theme::init);
let text = format!("outer\n inner\n{}\nend", vec![" child"; 100].join("\n"));
let (view, cx) = cx.add_window_view(move |window, cx| {
Boxed(cx.new(|cx| CodeEditor::new(text, window, cx).sticky_scroll()))
});
let editor = view.read_with(cx, |root, _| root.0.clone());
cx.run_until_parked();
editor.update(cx, |editor, cx| {
let at = editor.buffer.offset(50, 0);
let caret = at..at;
editor.select([caret], cx);
});
for _ in 0..3 {
cx.run_until_parked();
cx.update(|window, _| window.refresh());
}
let offset = editor.read_with(cx, |editor, _| {
editor.scroll.0.borrow().base_handle.offset()
});
assert!(
offset.y < gpui::Pixels::ZERO,
"the editor scrolled: {offset:?}"
);
editor.update(cx, |editor, cx| editor.set_text("x", cx));
cx.run_until_parked();
}
#[gpui::test]
fn a_point_on_a_row_the_text_lost_has_no_offset(cx: &mut TestAppContext) {
let (editor, cx) = editor("first\nsecond\nthird", cx);
cx.run_until_parked();
editor.update_in(cx, |editor, window, cx| {
let position = gpui::point(
editor.metrics.left,
editor.metrics.top + editor.metrics.line * 1.5,
);
assert_eq!(
editor.character_index_for_point(position, window, cx),
Some(6)
);
editor.set_text("x", cx);
assert_eq!(editor.character_index_for_point(position, window, cx), None);
});
cx.run_until_parked();
}
/// A press at a window point, sent as the platform would.
fn press(position: gpui::Point<gpui::Pixels>) -> gpui::PlatformInput {
gpui::PlatformInput::MouseDown(gpui::MouseDownEvent {
position,
button: gpui::MouseButton::Left,
modifiers: gpui::Modifiers::none(),
click_count: 1,
first_mouse: false,
})
}
#[gpui::test]
fn a_press_or_drag_on_a_row_the_text_lost_is_dropped(cx: &mut TestAppContext) {
let (editor, cx) = editor("first\nsecond\nthird", cx);
cx.run_until_parked();
let position = editor.read_with(cx, |editor, _| {
gpui::point(
editor.metrics.left + editor.metrics.advance,
editor.metrics.top + editor.metrics.line * 1.5,
)
});
cx.simulate_mouse_down(position, gpui::MouseButton::Left, gpui::Modifiers::none());
assert_eq!(editor.read_with(cx, |editor, _| editor.position().0), 2);
cx.update(|window, cx| {
editor.update(cx, |editor, cx| editor.set_text("x", cx));
let held = gpui::PlatformInput::MouseMove(gpui::MouseMoveEvent {
position,
pressed_button: Some(gpui::MouseButton::Left),
modifiers: gpui::Modifiers::none(),
});
window.dispatch_event(held, cx);
window.dispatch_event(press(position), cx);
});
cx.run_until_parked();
assert_eq!(editor.read_with(cx, |editor, _| editor.position().0), 1);
}
#[gpui::test]
fn a_sticky_press_on_a_line_the_text_lost_is_dropped(cx: &mut TestAppContext) {
use gpui::AppContext as _;
cx.update(Theme::init);
let text = format!("outer\n inner\n{}\nend", vec![" child"; 100].join("\n"));
let (view, cx) = cx.add_window_view(move |window, cx| {
Boxed(cx.new(|cx| CodeEditor::new(text, window, cx).sticky_scroll()))
});
let editor = view.read_with(cx, |root, _| root.0.clone());
cx.run_until_parked();
editor.update(cx, |editor, cx| {
let at = editor.buffer.offset(50, 0);
let caret = at..at;
editor.select([caret], cx);
});
for _ in 0..3 {
cx.run_until_parked();
cx.update(|window, _| window.refresh());
}
let position = editor.read_with(cx, |editor, _| {
gpui::point(
gpui::px(200.0),
editor.metrics.top + editor.metrics.line * 1.5,
)
});
cx.update(|window, cx| {
editor.update(cx, |editor, cx| editor.set_text("x", cx));
window.dispatch_event(press(position), cx);
});
cx.run_until_parked();
}
+19 -11
View File
@@ -85,6 +85,9 @@ impl CodeEditor {
.on_mouse_down(MouseButton::Left, move |_, window, cx| {
window.prevent_default();
jump.update(cx, |editor, cx| {
if line >= editor.buffer.lines() {
return;
}
let at = editor.buffer.line_range(line).start + editor.buffer.indent(line);
let cursor = at..at;
editor.select([cursor], cx);
@@ -219,23 +222,23 @@ impl CodeEditor {
impl Render for CodeEditor {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let sticky = if self.options.sticky {
self.sticky(cx)
} else {
None
};
let theme = cx.theme();
let colors = theme.colors.clone();
let mono_family = theme.mono_family.clone();
let size = theme.text_size(TextSize::Sm);
let pixels = size.to_pixels(window.rem_size());
let mono = window
.text_system()
.resolve_font(&font(theme.mono_family.clone()));
let advance = window
.text_system()
.advance(mono, pixels, 'm')
.expect("the code font has an m")
.width;
let advance = match window.text_system().advance(mono, pixels, 'm') {
Ok(advance) => advance.width,
Err(error) => {
log::error!(
"code editor: no advance for the code font: {error:#}; columns guessed"
);
pixels * 0.6
}
};
let line = (pixels * LEADING).round();
let found = folds(&self.buffer);
let hide = hidden(self.buffer.lines(), &found, &self.folded);
@@ -260,6 +263,11 @@ impl Render for CodeEditor {
};
self.metrics.advance = advance;
self.metrics.line = line;
let sticky = if self.options.sticky {
self.sticky(cx)
} else {
None
};
let count = self.frame.rows.len();
let list = uniform_list(
"editor-rows",
@@ -311,7 +319,7 @@ impl Render for CodeEditor {
.flex_col()
.overflow_hidden()
.bg(colors.surface)
.font_family(theme.mono_family.clone())
.font_family(mono_family)
.text_size(size)
.text_color(colors.fg)
.on_mouse_up(