diff options
author | Pascal Kuthe | 2023-01-31 17:03:19 +0000 |
---|---|---|
committer | GitHub | 2023-01-31 17:03:19 +0000 |
commit | 4dcf1fe66ba30a78edc054780d9b65c2f826530f (patch) | |
tree | ffb84ea94f07ceb52494a955b1bd78f115395dc0 /helix-view | |
parent | 4eca4b3079bf53de874959270d0b3471d320debc (diff) |
rework positioning/rendering and enable softwrap/virtual text (#5420)
* rework positioning/rendering, enables softwrap/virtual text
This commit is a large rework of the core text positioning and
rendering code in helix to remove the assumption that on-screen
columns/lines correspond to text columns/lines.
A generic `DocFormatter` is introduced that positions graphemes on
and is used both for rendering and for movements/scrolling.
Both virtual text support (inline, grapheme overlay and multi-line)
and a capable softwrap implementation is included.
fix picker highlight
cleanup doc formatter, use word bondaries for wrapping
make visual vertical movement a seperate commnad
estimate line gutter width to improve performance
cache cursor position
cleanup and optimize doc formatter
cleanup documentation
fix typos
Co-authored-by: Daniel Hines <d4hines@gmail.com>
update documentation
fix panic in last_visual_line funciton
improve soft-wrap documentation
add extend_visual_line_up/down commands
fix non-visual vertical movement
streamline virtual text highlighting, add softwrap indicator
fix cursor position if softwrap is disabled
improve documentation of text_annotations module
avoid crashes if view anchor is out of bounds
fix: consider horizontal offset when traslation char_idx -> vpos
improve default configuration
fix: mixed up horizontal and vertical offset
reset view position after config reload
apply suggestions from review
disabled softwrap for very small screens to avoid endless spin
fix wrap_indicator setting
fix bar cursor disappearring on the EOF character
add keybinding for linewise vertical movement
fix: inconsistent gutter highlights
improve virtual text API
make scope idx lookup more ergonomic
allow overlapping overlays
correctly track char_pos for virtual text
adjust configuration
deprecate old position fucntions
fix infinite loop in highlight lookup
fix gutter style
fix formatting
document max-line-width interaction with softwrap
change wrap-indicator example to use empty string
fix: rare panic when view is in invalid state (bis)
* Apply suggestions from code review
Co-authored-by: Michael Davis <mcarsondavis@gmail.com>
* improve documentation for positoning functions
* simplify tests
* fix documentation of Grapheme::width
* Apply suggestions from code review
Co-authored-by: Michael Davis <mcarsondavis@gmail.com>
* add explicit drop invocation
* Add explicit MoveFn type alias
* add docuntation to Editor::cursor_cache
* fix a few typos
* explain use of allow(deprecated)
* make gj and gk extend in select mode
* remove unneded debug and TODO
* mark tab_width_at #[inline]
* add fast-path to move_vertically_visual in case softwrap is disabled
* rename first_line to first_visual_line
* simplify duplicate if/else
---------
Co-authored-by: Michael Davis <mcarsondavis@gmail.com>
Diffstat (limited to 'helix-view')
-rw-r--r-- | helix-view/src/document.rs | 74 | ||||
-rw-r--r-- | helix-view/src/editor.rs | 85 | ||||
-rw-r--r-- | helix-view/src/gutter.rs | 293 | ||||
-rw-r--r-- | helix-view/src/lib.rs | 23 | ||||
-rw-r--r-- | helix-view/src/theme.rs | 15 | ||||
-rw-r--r-- | helix-view/src/view.rs | 647 |
6 files changed, 855 insertions, 282 deletions
diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index 6b33ea6a..798b5400 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -1,7 +1,11 @@ use anyhow::{anyhow, bail, Context, Error}; +use arc_swap::access::DynAccess; use futures_util::future::BoxFuture; use futures_util::FutureExt; use helix_core::auto_pairs::AutoPairs; +use helix_core::doc_formatter::TextFormat; +use helix_core::syntax::Highlight; +use helix_core::text_annotations::TextAnnotations; use helix_core::Range; use helix_vcs::{DiffHandle, DiffProviderRegistry}; @@ -26,8 +30,8 @@ use helix_core::{ DEFAULT_LINE_ENDING, }; -use crate::editor::RedrawHandle; -use crate::{DocumentId, Editor, View, ViewId}; +use crate::editor::{Config, RedrawHandle}; +use crate::{DocumentId, Editor, Theme, View, ViewId}; /// 8kB of buffer space for encoding and decoding `Rope`s. const BUF_SIZE: usize = 8192; @@ -127,6 +131,7 @@ pub struct Document { // it back as it separated from the edits. We could split out the parts manually but that will // be more troublesome. pub history: Cell<History>, + pub config: Arc<dyn DynAccess<Config>>, pub savepoint: Option<Transaction>, @@ -351,7 +356,11 @@ use helix_lsp::lsp; use url::Url; impl Document { - pub fn from(text: Rope, encoding: Option<&'static encoding::Encoding>) -> Self { + pub fn from( + text: Rope, + encoding: Option<&'static encoding::Encoding>, + config: Arc<dyn DynAccess<Config>>, + ) -> Self { let encoding = encoding.unwrap_or(encoding::UTF_8); let changes = ChangeSet::new(&text); let old_state = None; @@ -377,9 +386,13 @@ impl Document { modified_since_accessed: false, language_server: None, diff_handle: None, + config, } } - + pub fn default(config: Arc<dyn DynAccess<Config>>) -> Self { + let text = Rope::from(DEFAULT_LINE_ENDING.as_str()); + Self::from(text, None, config) + } // TODO: async fn? /// Create a new document from `path`. Encoding is auto-detected, but it can be manually /// overwritten with the `encoding` parameter. @@ -387,6 +400,7 @@ impl Document { path: &Path, encoding: Option<&'static encoding::Encoding>, config_loader: Option<Arc<syntax::Loader>>, + config: Arc<dyn DynAccess<Config>>, ) -> Result<Self, Error> { // Open the file if it exists, otherwise assume it is a new file (and thus empty). let (rope, encoding) = if path.exists() { @@ -398,7 +412,7 @@ impl Document { (Rope::from(DEFAULT_LINE_ENDING.as_str()), encoding) }; - let mut doc = Self::from(rope, Some(encoding)); + let mut doc = Self::from(rope, Some(encoding), config); // set the path and try detecting the language doc.set_path(Some(path))?; @@ -1192,12 +1206,34 @@ impl Document { None => global_config, } } -} -impl Default for Document { - fn default() -> Self { - let text = Rope::from(DEFAULT_LINE_ENDING.as_str()); - Self::from(text, None) + pub fn text_format(&self, mut viewport_width: u16, theme: Option<&Theme>) -> TextFormat { + if let Some(max_line_len) = self + .language_config() + .and_then(|config| config.max_line_length) + { + viewport_width = viewport_width.min(max_line_len as u16) + } + let config = self.config.load(); + let soft_wrap = &config.soft_wrap; + let tab_width = self.tab_width() as u16; + TextFormat { + soft_wrap: soft_wrap.enable && viewport_width > 10, + tab_width, + max_wrap: soft_wrap.max_wrap.min(viewport_width / 4), + max_indent_retain: soft_wrap.max_indent_retain.min(viewport_width * 2 / 5), + // avoid spinning forever when the window manager + // sets the size to something tiny + viewport_width, + wrap_indicator: soft_wrap.wrap_indicator.clone().into_boxed_str(), + wrap_indicator_highlight: theme + .and_then(|theme| theme.find_scope_index("ui.virtual.wrap")) + .map(Highlight), + } + } + + pub fn text_annotations(&self, _theme: Option<&Theme>) -> TextAnnotations { + TextAnnotations::default() } } @@ -1236,13 +1272,19 @@ impl Display for FormatterError { #[cfg(test)] mod test { + use arc_swap::ArcSwap; + use super::*; #[test] fn changeset_to_changes_ignore_line_endings() { use helix_lsp::{lsp, Client, OffsetEncoding}; let text = Rope::from("hello\r\nworld"); - let mut doc = Document::from(text, None); + let mut doc = Document::from( + text, + None, + Arc::new(ArcSwap::new(Arc::new(Config::default()))), + ); let view = ViewId::default(); doc.set_selection(view, Selection::single(0, 0)); @@ -1276,7 +1318,11 @@ mod test { fn changeset_to_changes() { use helix_lsp::{lsp, Client, OffsetEncoding}; let text = Rope::from("hello"); - let mut doc = Document::from(text, None); + let mut doc = Document::from( + text, + None, + Arc::new(ArcSwap::new(Arc::new(Config::default()))), + ); let view = ViewId::default(); doc.set_selection(view, Selection::single(5, 5)); @@ -1389,7 +1435,9 @@ mod test { #[test] fn test_line_ending() { assert_eq!( - Document::default().text().to_string(), + Document::default(Arc::new(ArcSwap::new(Arc::new(Config::default())))) + .text() + .to_string(), DEFAULT_LINE_ENDING.as_str() ); } diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 1029c14f..46511c62 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -7,6 +7,7 @@ use crate::{ input::KeyEvent, theme::{self, Theme}, tree::{self, Tree}, + view::ViewPosition, Align, Document, DocumentId, View, ViewId, }; use helix_vcs::DiffProviderRegistry; @@ -18,6 +19,7 @@ use tokio_stream::wrappers::UnboundedReceiverStream; use std::{ borrow::Cow, + cell::Cell, collections::{BTreeMap, HashMap}, io::stdin, num::NonZeroUsize, @@ -268,6 +270,44 @@ pub struct Config { pub indent_guides: IndentGuidesConfig, /// Whether to color modes with different colors. Defaults to `false`. pub color_modes: bool, + pub soft_wrap: SoftWrap, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(default, rename_all = "kebab-case", deny_unknown_fields)] +pub struct SoftWrap { + /// Soft wrap lines that exceed viewport width. Default to off + pub enable: bool, + /// Maximum space left free at the end of the line. + /// This space is used to wrap text at word boundaries. If that is not possible within this limit + /// the word is simply split at the end of the line. + /// + /// This is automatically hard-limited to a quarter of the viewport to ensure correct display on small views. + /// + /// Default to 20 + pub max_wrap: u16, + /// Maximum number of indentation that can be carried over from the previous line when softwrapping. + /// If a line is indented further then this limit it is rendered at the start of the viewport instead. + /// + /// This is automatically hard-limited to a quarter of the viewport to ensure correct display on small views. + /// + /// Default to 40 + pub max_indent_retain: u16, + /// Indicator placed at the beginning of softwrapped lines + /// + /// Defaults to ↪ + pub wrap_indicator: String, +} + +impl Default for SoftWrap { + fn default() -> Self { + SoftWrap { + enable: false, + max_wrap: 20, + max_indent_retain: 40, + wrap_indicator: "↪ ".into(), + } + } } #[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -717,6 +757,7 @@ impl Default for Config { bufferline: BufferLine::default(), indent_guides: IndentGuidesConfig::default(), color_modes: false, + soft_wrap: SoftWrap::default(), } } } @@ -797,7 +838,7 @@ pub struct Editor { pub status_msg: Option<(Cow<'static, str>, Severity)>, pub autoinfo: Option<Info>, - pub config: Box<dyn DynAccess<Config>>, + pub config: Arc<dyn DynAccess<Config>>, pub auto_pairs: Option<AutoPairs>, pub idle_timer: Pin<Box<Sleep>>, @@ -813,6 +854,19 @@ pub struct Editor { /// The `RwLock` blocks the editor from performing the render until an exclusive lock can be aquired pub redraw_handle: RedrawHandle, pub needs_redraw: bool, + /// Cached position of the cursor calculated during rendering. + /// The content of `cursor_cache` is returned by `Editor::cursor` if + /// set to `Some(_)`. The value will be cleared after it's used. + /// If `cursor_cache` is `None` then the `Editor::cursor` function will + /// calculate the cursor position. + /// + /// `Some(None)` represents a cursor position outside of the visible area. + /// This will just cause `Editor::cursor` to return `None`. + /// + /// This cache is only a performance optimization to + /// avoid calculating the cursor position multiple + /// times during rendering and should not be set by other functions. + pub cursor_cache: Cell<Option<Option<Position>>>, } pub type RedrawHandle = (Arc<Notify>, Arc<RwLock<()>>); @@ -866,7 +920,7 @@ impl Editor { mut area: Rect, theme_loader: Arc<theme::Loader>, syn_loader: Arc<syntax::Loader>, - config: Box<dyn DynAccess<Config>>, + config: Arc<dyn DynAccess<Config>>, ) -> Self { let conf = config.load(); let auto_pairs = (&conf.auto_pairs).into(); @@ -910,6 +964,7 @@ impl Editor { config_events: unbounded_channel(), redraw_handle: Default::default(), needs_redraw: false, + cursor_cache: Cell::new(None), } } @@ -994,7 +1049,7 @@ impl Editor { fn set_theme_impl(&mut self, theme: Theme, preview: ThemeAction) { // `ui.selection` is the only scope required to be able to render a theme. - if theme.find_scope_index("ui.selection").is_none() { + if theme.find_scope_index_exact("ui.selection").is_none() { self.set_error("Invalid theme: `ui.selection` required"); return; } @@ -1077,7 +1132,7 @@ impl Editor { fn replace_document_in_view(&mut self, current_view: ViewId, doc_id: DocumentId) { let view = self.tree.get_mut(current_view); view.doc = doc_id; - view.offset = Position::default(); + view.offset = ViewPosition::default(); let doc = doc_mut!(self, &doc_id); doc.ensure_view_init(view.id); @@ -1204,12 +1259,15 @@ impl Editor { } pub fn new_file(&mut self, action: Action) -> DocumentId { - self.new_file_from_document(action, Document::default()) + self.new_file_from_document(action, Document::default(self.config.clone())) } pub fn new_file_from_stdin(&mut self, action: Action) -> Result<DocumentId, Error> { let (rope, encoding) = crate::document::from_reader(&mut stdin(), None)?; - Ok(self.new_file_from_document(action, Document::from(rope, Some(encoding)))) + Ok(self.new_file_from_document( + action, + Document::from(rope, Some(encoding), self.config.clone()), + )) } // ??? possible use for integration tests @@ -1220,7 +1278,12 @@ impl Editor { let id = if let Some(id) = id { id } else { - let mut doc = Document::open(&path, None, Some(self.syn_loader.clone()))?; + let mut doc = Document::open( + &path, + None, + Some(self.syn_loader.clone()), + self.config.clone(), + )?; let _ = Self::launch_language_server(&mut self.language_servers, &mut doc); if let Some(diff_base) = self.diff_providers.get_diff_base(&path) { @@ -1306,7 +1369,7 @@ impl Editor { .iter() .map(|(&doc_id, _)| doc_id) .next() - .unwrap_or_else(|| self.new_document(Document::default())); + .unwrap_or_else(|| self.new_document(Document::default(self.config.clone()))); let view = View::new(doc_id, self.config().gutters.clone()); let view_id = self.tree.insert(view); let doc = doc_mut!(self, &doc_id); @@ -1440,7 +1503,11 @@ impl Editor { .selection(view.id) .primary() .cursor(doc.text().slice(..)); - if let Some(mut pos) = view.screen_coords_at_pos(doc, doc.text().slice(..), cursor) { + let pos = self + .cursor_cache + .get() + .unwrap_or_else(|| view.screen_coords_at_pos(doc, doc.text().slice(..), cursor)); + if let Some(mut pos) = pos { let inner = view.inner_area(doc); pos.col += inner.x as usize; pos.row += inner.y as usize; diff --git a/helix-view/src/gutter.rs b/helix-view/src/gutter.rs index c1b5e2b1..90c94d55 100644 --- a/helix-view/src/gutter.rs +++ b/helix-view/src/gutter.rs @@ -12,7 +12,7 @@ fn count_digits(n: usize) -> usize { std::iter::successors(Some(n), |&n| (n >= 10).then(|| n / 10)).count() } -pub type GutterFn<'doc> = Box<dyn FnMut(usize, bool, &mut String) -> Option<Style> + 'doc>; +pub type GutterFn<'doc> = Box<dyn FnMut(usize, bool, bool, &mut String) -> Option<Style> + 'doc>; pub type Gutter = for<'doc> fn(&'doc Editor, &'doc Document, &View, &Theme, bool, usize) -> GutterFn<'doc>; @@ -58,31 +58,36 @@ pub fn diagnostic<'doc>( let hint = theme.get("hint"); let diagnostics = doc.diagnostics(); - Box::new(move |line: usize, _selected: bool, out: &mut String| { - use helix_core::diagnostic::Severity; - if let Ok(index) = diagnostics.binary_search_by_key(&line, |d| d.line) { - let after = diagnostics[index..].iter().take_while(|d| d.line == line); - - let before = diagnostics[..index] - .iter() - .rev() - .take_while(|d| d.line == line); - - let diagnostics_on_line = after.chain(before); - - // This unwrap is safe because the iterator cannot be empty as it contains at least the item found by the binary search. - let diagnostic = diagnostics_on_line.max_by_key(|d| d.severity).unwrap(); - - write!(out, "●").unwrap(); - return Some(match diagnostic.severity { - Some(Severity::Error) => error, - Some(Severity::Warning) | None => warning, - Some(Severity::Info) => info, - Some(Severity::Hint) => hint, - }); - } - None - }) + Box::new( + move |line: usize, _selected: bool, first_visual_line: bool, out: &mut String| { + if !first_visual_line { + return None; + } + use helix_core::diagnostic::Severity; + if let Ok(index) = diagnostics.binary_search_by_key(&line, |d| d.line) { + let after = diagnostics[index..].iter().take_while(|d| d.line == line); + + let before = diagnostics[..index] + .iter() + .rev() + .take_while(|d| d.line == line); + + let diagnostics_on_line = after.chain(before); + + // This unwrap is safe because the iterator cannot be empty as it contains at least the item found by the binary search. + let diagnostic = diagnostics_on_line.max_by_key(|d| d.severity).unwrap(); + + write!(out, "●").unwrap(); + return Some(match diagnostic.severity { + Some(Severity::Error) => error, + Some(Severity::Warning) | None => warning, + Some(Severity::Info) => info, + Some(Severity::Hint) => hint, + }); + } + None + }, + ) } pub fn diff<'doc>( @@ -99,36 +104,41 @@ pub fn diff<'doc>( let hunks = diff_handle.hunks(); let mut hunk_i = 0; let mut hunk = hunks.nth_hunk(hunk_i); - Box::new(move |line: usize, _selected: bool, out: &mut String| { - // truncating the line is fine here because we don't compute diffs - // for files with more lines than i32::MAX anyways - // we need to special case removals here - // these technically do not have a range of lines to highlight (`hunk.after.start == hunk.after.end`). - // However we still want to display these hunks correctly we must not yet skip to the next hunk here - while hunk.after.end < line as u32 - || !hunk.is_pure_removal() && line as u32 == hunk.after.end - { - hunk_i += 1; - hunk = hunks.nth_hunk(hunk_i); - } - - if hunk.after.start > line as u32 { - return None; - } - - let (icon, style) = if hunk.is_pure_insertion() { - ("▍", added) - } else if hunk.is_pure_removal() { - ("▔", deleted) - } else { - ("▍", modified) - }; - - write!(out, "{}", icon).unwrap(); - Some(style) - }) + Box::new( + move |line: usize, _selected: bool, first_visual_line: bool, out: &mut String| { + // truncating the line is fine here because we don't compute diffs + // for files with more lines than i32::MAX anyways + // we need to special case removals here + // these technically do not have a range of lines to highlight (`hunk.after.start == hunk.after.end`). + // However we still want to display these hunks correctly we must not yet skip to the next hunk here + while hunk.after.end < line as u32 + || !hunk.is_pure_removal() && line as u32 == hunk.after.end + { + hunk_i += 1; + hunk = hunks.nth_hunk(hunk_i); + } + + if hunk.after.start > line as u32 { + return None; + } + + let (icon, style) = if hunk.is_pure_insertion() { + ("▍", added) + } else if hunk.is_pure_removal() { + if !first_visual_line { + return None; + } + ("▔", deleted) + } else { + ("▍", modified) + }; + + write!(out, "{}", icon).unwrap(); + Some(style) + }, + ) } else { - Box::new(move |_, _, _| None) + Box::new(move |_, _, _, _| None) } } @@ -142,7 +152,7 @@ pub fn line_numbers<'doc>( let text = doc.text().slice(..); let width = line_numbers_width(view, doc); - let last_line_in_view = view.last_line(doc); + let last_line_in_view = view.estimate_last_doc_line(doc); // Whether to draw the line number for the last line of the // document or not. We only draw it if it's not an empty line. @@ -158,34 +168,42 @@ pub fn line_numbers<'doc>( let line_number = editor.config().line_number; let mode = editor.mode; - Box::new(move |line: usize, selected: bool, out: &mut String| { - if line == last_line_in_view && !draw_last { - write!(out, "{:>1$}", '~', width).unwrap(); - Some(linenr) - } else { - use crate::{document::Mode, editor::LineNumber}; - - let relative = line_number == LineNumber::Relative - && mode != Mode::Insert - && is_focused - && current_line != line; - - let display_num = if relative { - abs_diff(current_line, line) + Box::new( + move |line: usize, selected: bool, first_visual_line: bool, out: &mut String| { + if line == last_line_in_view && !draw_last { + write!(out, "{:>1$}", '~', width).unwrap(); + Some(linenr) } else { - line + 1 - }; - - let style = if selected && is_focused { - linenr_select - } else { - linenr - }; - - write!(out, "{:>1$}", display_num, width).unwrap(); - Some(style) - } - }) + use crate::{document::Mode, editor::LineNumber}; + + let relative = line_number == LineNumber::Relative + && mode != Mode::Insert + && is_focused + && current_line != line; + + let display_num = if relative { + abs_diff(current_line, line) + } else { + line + 1 + }; + + let style = if selected && is_focused { + linenr_select + } else { + linenr + }; + + if first_visual_line { + write!(out, "{:>1$}", display_num, width).unwrap(); + } else { + write!(out, "{:>1$}", " ", width).unwrap(); + } + + // TODO: Use then_some when MSRV reaches 1.62 + first_visual_line.then(|| style) + } + }, + ) } /// The width of a "line-numbers" gutter @@ -210,7 +228,7 @@ pub fn padding<'doc>( _theme: &Theme, _is_focused: bool, ) -> GutterFn<'doc> { - Box::new(|_line: usize, _selected: bool, _out: &mut String| None) + Box::new(|_line: usize, _selected: bool, _first_visual_line: bool, _out: &mut String| None) } #[inline(always)] @@ -237,41 +255,46 @@ pub fn breakpoints<'doc>( let breakpoints = match breakpoints { Some(breakpoints) => breakpoints, - None => return Box::new(move |_, _, _| None), + None => return Box::new(move |_, _, _, _| None), }; - Box::new(move |line: usize, _selected: bool, out: &mut String| { - let breakpoint = breakpoints - .iter() - .find(|breakpoint| breakpoint.line == line)?; - - let mut style = if breakpoint.condition.is_some() && breakpoint.log_message.is_some() { - error.underline_style(UnderlineStyle::Line) - } else if breakpoint.condition.is_some() { - error - } else if breakpoint.log_message.is_some() { - info - } else { - warning - }; - - if !breakpoint.verified { - // Faded colors - style = if let Some(Color::Rgb(r, g, b)) = style.fg { - style.fg(Color::Rgb( - ((r as f32) * 0.4).floor() as u8, - ((g as f32) * 0.4).floor() as u8, - ((b as f32) * 0.4).floor() as u8, - )) - } else { - style.fg(Color::Gray) + Box::new( + move |line: usize, _selected: bool, first_visual_line: bool, out: &mut String| { + if !first_visual_line { + return None; } - }; + let breakpoint = breakpoints + .iter() + .find(|breakpoint| breakpoint.line == line)?; + + let mut style = if breakpoint.condition.is_some() && breakpoint.log_message.is_some() { + error.underline_style(UnderlineStyle::Line) + } else if breakpoint.condition.is_some() { + error + } else if breakpoint.log_message.is_some() { + info + } else { + warning + }; - let sym = if breakpoint.verified { "▲" } else { "⊚" }; - write!(out, "{}", sym).unwrap(); - Some(style) - }) + if !breakpoint.verified { + // Faded colors + style = if let Some(Color::Rgb(r, g, b)) = style.fg { + style.fg(Color::Rgb( + ((r as f32) * 0.4).floor() as u8, + ((g as f32) * 0.4).floor() as u8, + ((b as f32) * 0.4).floor() as u8, + )) + } else { + style.fg(Color::Gray) + } + }; + + let sym = if breakpoint.verified { "▲" } else { "⊚" }; + write!(out, "{}", sym).unwrap(); + Some(style) + }, + ) } pub fn diagnostics_or_breakpoints<'doc>( @@ -284,18 +307,22 @@ pub fn diagnostics_or_breakpoints<'doc>( let mut diagnostics = diagnostic(editor, doc, view, theme, is_focused); let mut breakpoints = breakpoints(editor, doc, view, theme, is_focused); - Box::new(move |line, selected, out| { - breakpoints(line, selected, out).or_else(|| diagnostics(line, selected, out)) + Box::new(move |line, selected, first_visual_line: bool, out| { + breakpoints(line, selected, first_visual_line, out) + .or_else(|| diagnostics(line, selected, first_visual_line, out)) }) } #[cfg(test)] mod tests { + use std::sync::Arc; + use super::*; use crate::document::Document; - use crate::editor::{GutterConfig, GutterLineNumbersConfig}; + use crate::editor::{Config, GutterConfig, GutterLineNumbersConfig}; use crate::graphics::Rect; use crate::DocumentId; + use arc_swap::ArcSwap; use helix_core::Rope; #[test] @@ -304,7 +331,11 @@ mod tests { view.area = Rect::new(40, 40, 40, 40); let rope = Rope::from_str("abc\n\tdef"); - let doc = Document::from(rope, None); + let doc = Document::from( + rope, + None, + Arc::new(ArcSwap::new(Arc::new(Config::default()))), + ); assert_eq!(view.gutters.layout.len(), 5); assert_eq!(view.gutters.layout[0].width(&view, &doc), 1); @@ -325,7 +356,11 @@ mod tests { view.area = Rect::new(40, 40, 40, 40); let rope = Rope::from_str("abc\n\tdef"); - let doc = Document::from(rope, None); + let doc = Document::from( + rope, + None, + Arc::new(ArcSwap::new(Arc::new(Config::default()))), + ); assert_eq!(view.gutters.layout.len(), 1); assert_eq!(view.gutters.layout[0].width(&view, &doc), 1); @@ -339,7 +374,11 @@ mod tests { view.area = Rect::new(40, 40, 40, 40); let rope = Rope::from_str("abc\n\tdef"); - let doc = Document::from(rope, None); + let doc = Document::from( + rope, + None, + Arc::new(ArcSwap::new(Arc::new(Config::default()))), + ); assert_eq!(view.gutters.layout.len(), 2); assert_eq!(view.gutters.layout[0].width(&view, &doc), 1); @@ -357,10 +396,18 @@ mod tests { view.area = Rect::new(40, 40, 40, 40); let rope = Rope::from_str("a\nb"); - let doc_short = Document::from(rope, None); + let doc_short = Document::from( + rope, + None, + Arc::new(ArcSwap::new(Arc::new(Config::default()))), + ); let rope = Rope::from_str("a\nb\nc\nd\ne\nf\ng\nh\ni\nj\nk\nl\nm\nn\no\np"); - let doc_long = Document::from(rope, None); + let doc_long = Document::from( + rope, + None, + Arc::new(ArcSwap::new(Arc::new(Config::default()))), + ); assert_eq!(view.gutters.layout.len(), 2); assert_eq!(view.gutters.layout[1].width(&view, &doc_short), 1); diff --git a/helix-view/src/lib.rs b/helix-view/src/lib.rs index 9a980446..c3f67345 100644 --- a/helix-view/src/lib.rs +++ b/helix-view/src/lib.rs @@ -49,13 +49,10 @@ pub enum Align { } pub fn align_view(doc: &Document, view: &mut View, align: Align) { - let pos = doc - .selection(view.id) - .primary() - .cursor(doc.text().slice(..)); - let line = doc.text().char_to_line(pos); - - let last_line_height = view.inner_height().saturating_sub(1); + let doc_text = doc.text().slice(..); + let cursor = doc.selection(view.id).primary().cursor(doc_text); + let viewport = view.inner_area(doc); + let last_line_height = viewport.height.saturating_sub(1); let relative = match align { Align::Center => last_line_height / 2, @@ -63,10 +60,20 @@ pub fn align_view(doc: &Document, view: &mut View, align: Align) { Align::Bottom => last_line_height, }; - view.offset.row = line.saturating_sub(relative); + let text_fmt = doc.text_format(viewport.width, None); + let annotations = view.text_annotations(doc, None); + (view.offset.anchor, view.offset.vertical_offset) = char_idx_at_visual_offset( + doc_text, + cursor, + -(relative as isize), + 0, + &text_fmt, + &annotations, + ); } pub use document::Document; pub use editor::Editor; +use helix_core::char_idx_at_visual_offset; pub use theme::Theme; pub use view::View; diff --git a/helix-view/src/theme.rs b/helix-view/src/theme.rs index 125725e0..ce061bab 100644 --- a/helix-view/src/theme.rs +++ b/helix-view/src/theme.rs @@ -314,10 +314,23 @@ impl Theme { &self.scopes } - pub fn find_scope_index(&self, scope: &str) -> Option<usize> { + pub fn find_scope_index_exact(&self, scope: &str) -> Option<usize> { self.scopes().iter().position(|s| s == scope) } + pub fn find_scope_index(&self, mut scope: &str) -> Option<usize> { + loop { + if let Some(highlight) = self.find_scope_index_exact(scope) { + return Some(highlight); + } + if let Some(new_end) = scope.rfind('.') { + scope = &scope[..new_end]; + } else { + return None; + } + } + } + pub fn is_16_color(&self) -> bool { self.styles.iter().all(|(_, style)| { [style.fg, style.bg] diff --git a/helix-view/src/view.rs b/helix-view/src/view.rs index abcf9a16..660cce65 100644 --- a/helix-view/src/view.rs +++ b/helix-view/src/view.rs @@ -2,11 +2,13 @@ use crate::{ align_view, editor::{GutterConfig, GutterType}, graphics::Rect, - Align, Document, DocumentId, ViewId, + Align, Document, DocumentId, Theme, ViewId, }; use helix_core::{ - pos_at_visual_coords, visual_coords_at_pos, Position, RopeSlice, Selection, Transaction, + char_idx_at_visual_offset, doc_formatter::TextFormat, text_annotations::TextAnnotations, + visual_offset_from_anchor, visual_offset_from_block, Position, RopeSlice, Selection, + Transaction, }; use std::{ @@ -93,10 +95,17 @@ impl JumpList { } } +#[derive(Clone, Debug, PartialEq, Eq, Copy, Default)] +pub struct ViewPosition { + pub anchor: usize, + pub horizontal_offset: usize, + pub vertical_offset: usize, +} + #[derive(Clone)] pub struct View { pub id: ViewId, - pub offset: Position, + pub offset: ViewPosition, pub area: Rect, pub doc: DocumentId, pub jumps: JumpList, @@ -133,7 +142,11 @@ impl View { Self { id: ViewId::default(), doc, - offset: Position::new(0, 0), + offset: ViewPosition { + anchor: 0, + horizontal_offset: 0, + vertical_offset: 0, + }, area: Rect::default(), // will get calculated upon inserting into tree jumps: JumpList::new((doc, Selection::point(0))), // TODO: use actual sel docs_access_history: Vec::new(), @@ -159,6 +172,10 @@ impl View { self.area.clip_bottom(1).height.into() // -1 for statusline } + pub fn inner_width(&self, doc: &Document) -> u16 { + self.area.clip_left(self.gutter_offset(doc)).width + } + pub fn gutters(&self) -> &[GutterType] { &self.gutters.layout } @@ -176,84 +193,120 @@ impl View { &self, doc: &Document, scrolloff: usize, - ) -> Option<(usize, usize)> { - self.offset_coords_to_in_view_center(doc, scrolloff, false) + ) -> Option<ViewPosition> { + self.offset_coords_to_in_view_center::<false>(doc, scrolloff) } - pub fn offset_coords_to_in_view_center( + pub fn offset_coords_to_in_view_center<const CENTERING: bool>( &self, doc: &Document, scrolloff: usize, - centering: bool, - ) -> Option<(usize, usize)> { - let cursor = doc - .selection(self.id) - .primary() - .cursor(doc.text().slice(..)); - - let Position { col, row: line } = - visual_coords_at_pos(doc.text().slice(..), cursor, doc.tab_width()); - - let inner_area = self.inner_area(doc); - let last_line = (self.offset.row + inner_area.height as usize).saturating_sub(1); - let last_col = self.offset.col + inner_area.width.saturating_sub(1) as usize; - - let new_offset = |scrolloff: usize| { - // - 1 so we have at least one gap in the middle. - // a height of 6 with padding of 3 on each side will keep shifting the view back and forth - // as we type - let scrolloff = scrolloff.min(inner_area.height.saturating_sub(1) as usize / 2); - - let row = if line > last_line.saturating_sub(scrolloff) { - // scroll down - self.offset.row + line - (last_line.saturating_sub(scrolloff)) - } else if line < self.offset.row + scrolloff { - // scroll up - line.saturating_sub(scrolloff) + ) -> Option<ViewPosition> { + let doc_text = doc.text().slice(..); + let viewport = self.inner_area(doc); + let vertical_viewport_end = self.offset.vertical_offset + viewport.height as usize; + let text_fmt = doc.text_format(viewport.width, None); + let annotations = self.text_annotations(doc, None); + + // - 1 so we have at least one gap in the middle. + // a height of 6 with padding of 3 on each side will keep shifting the view back and forth + // as we type + let scrolloff = scrolloff.min(viewport.height.saturating_sub(1) as usize / 2); + + let cursor = doc.selection(self.id).primary().cursor(doc_text); + let mut offset = self.offset; + + let (visual_off, mut at_top) = if cursor >= offset.anchor { + let off = visual_offset_from_anchor( + doc_text, + offset.anchor, + cursor, + &text_fmt, + &annotations, + vertical_viewport_end, + ); + (off, false) + } else if CENTERING { + // cursor out of view + return None; + } else { + (None, true) + }; + + let new_anchor = match visual_off { + Some((visual_pos, _)) if visual_pos.row < scrolloff + offset.vertical_offset => { + if CENTERING && visual_pos.row < offset.vertical_offset { + // cursor out of view + return None; + } + at_top = true; + true + } + Some((visual_pos, _)) if visual_pos.row >= vertical_viewport_end - scrolloff => { + if CENTERING && visual_pos.row >= vertical_viewport_end as usize { + // cursor out of view + return None; + } + true + } + Some(_) => false, + None => true, + }; + + if new_anchor { + let v_off = if at_top { + scrolloff as isize } else { - self.offset.row + viewport.height as isize - scrolloff as isize }; + (offset.anchor, offset.vertical_offset) = + char_idx_at_visual_offset(doc_text, cursor, -v_off, 0, &text_fmt, &annotations); + } - let col = if col > last_col.saturating_sub(scrolloff) { + if text_fmt.soft_wrap { + offset.horizontal_offset = 0; + } else { + // determine the current visual column of the text + let col = visual_off + .unwrap_or_else(|| { + visual_offset_from_block( + doc_text, + offset.anchor, + cursor, + &text_fmt, + &annotations, + ) + }) + .0 + .col; + + let last_col = offset.horizontal_offset + viewport.width.saturating_sub(1) as usize; + if col > last_col.saturating_sub(scrolloff) { // scroll right - self.offset.col + col - (last_col.saturating_sub(scrolloff)) - } else if col < self.offset.col + scrolloff { + offset.horizontal_offset += col - (last_col.saturating_sub(scrolloff)) + } else if col < offset.horizontal_offset + scrolloff { // scroll left - col.saturating_sub(scrolloff) - } else { - self.offset.col + offset.horizontal_offset = col.saturating_sub(scrolloff) }; - (row, col) - }; - let current_offset = (self.offset.row, self.offset.col); - if centering { - // return None if cursor is out of view - let offset = new_offset(0); - (offset == current_offset).then(|| { - if scrolloff == 0 { - offset - } else { - new_offset(scrolloff) - } - }) - } else { - // return None if cursor is in (view - scrolloff) - let offset = new_offset(scrolloff); - (offset != current_offset).then(|| offset) // TODO: use 'then_some' when 1.62 <= MSRV } + + // if we are not centering return None if view position is unchanged + if !CENTERING && offset == self.offset { + return None; + } + + Some(offset) } pub fn ensure_cursor_in_view(&mut self, doc: &Document, scrolloff: usize) { - if let Some((row, col)) = self.offset_coords_to_in_view_center(doc, scrolloff, false) { - self.offset.row = row; - self.offset.col = col; + if let Some(offset) = self.offset_coords_to_in_view_center::<false>(doc, scrolloff) { + self.offset = offset; } } pub fn ensure_cursor_in_view_center(&mut self, doc: &Document, scrolloff: usize) { - if let Some((row, col)) = self.offset_coords_to_in_view_center(doc, scrolloff, true) { - self.offset.row = row; - self.offset.col = col; + if let Some(offset) = self.offset_coords_to_in_view_center::<true>(doc, scrolloff) { + self.offset = offset; } else { align_view(doc, self, Align::Center); } @@ -263,14 +316,51 @@ impl View { self.offset_coords_to_in_view(doc, scrolloff).is_none() } - /// Calculates the last visible line on screen + /// Estimates the last visible document line on screen. + /// This estimate is an upper bound obtained by calculating the first + /// visible line and adding the viewport height. + /// The actual last visible line may be smaller if softwrapping occurs + /// or virtual text lines are visible #[inline] - pub fn last_line(&self, doc: &Document) -> usize { - std::cmp::min( - // Saturating subs to make it inclusive zero indexing. - (self.offset.row + self.inner_height()).saturating_sub(1), - doc.text().len_lines().saturating_sub(1), - ) + pub fn estimate_last_doc_line(&self, doc: &Document) -> usize { + let doc_text = doc.text().slice(..); + let line = doc_text.char_to_line(self.offset.anchor.min(doc_text.len_chars())); + // Saturating subs to make it inclusive zero indexing. + (line + self.inner_height()) + .min(doc_text.len_lines()) + .saturating_sub(1) + } + + /// Calculates the last non-empty visual line on screen + #[inline] + pub fn last_visual_line(&self, doc: &Document) -> usize { + let doc_text = doc.text().slice(..); + let viewport = self.inner_area(doc); + let text_fmt = doc.text_format(viewport.width, None); + let annotations = self.text_annotations(doc, None); + + // last visual line in view is trivial to compute + let visual_height = self.offset.vertical_offset + viewport.height as usize; + + // fast path when the EOF is not visible on the screen, + if self.estimate_last_doc_line(doc) < doc_text.len_lines() - 1 { + return visual_height.saturating_sub(1); + } + + // translate to document line + let pos = visual_offset_from_anchor( + doc_text, + self.offset.anchor, + usize::MAX, + &text_fmt, + &annotations, + visual_height, + ); + + match pos { + Some((Position { row, .. }, _)) => row.saturating_sub(self.offset.vertical_offset), + None => visual_height.saturating_sub(1), + } } /// Translates a document position to an absolute position in the terminal. @@ -282,22 +372,39 @@ impl View { text: RopeSlice, pos: usize, ) -> Option<Position> { - let line = text.char_to_line(pos); - - if line < self.offset.row || line > self.last_line(doc) { + if pos < self.offset.anchor { // Line is not visible on screen return None; } - let tab_width = doc.tab_width(); - // TODO: visual_coords_at_pos also does char_to_line which we ignore, can we reuse the call? - let Position { col, .. } = visual_coords_at_pos(text, pos, tab_width); + let viewport = self.inner_area(doc); + let text_fmt = doc.text_format(viewport.width, None); + let annotations = self.text_annotations(doc, None); + + let mut pos = visual_offset_from_anchor( + text, + self.offset.anchor, + pos, + &text_fmt, + &annotations, + viewport.height as usize, + )? + .0; + if pos.row < self.offset.vertical_offset { + return None; + } + pos.row -= self.offset.vertical_offset; + if pos.row >= viewport.height as usize { + return None; + } + pos.col = pos.col.saturating_sub(self.offset.horizontal_offset); - // It is possible for underflow to occur if the buffer length is larger than the terminal width. - let row = line.saturating_sub(self.offset.row); - let col = col.saturating_sub(self.offset.col); + Some(pos) + } - Some(Position::new(row, col)) + pub fn text_annotations(&self, doc: &Document, theme: Option<&Theme>) -> TextAnnotations { + // TODO custom annotations for custom views like side by side diffs + doc.text_annotations(theme) } pub fn text_pos_at_screen_coords( @@ -305,9 +412,10 @@ impl View { doc: &Document, row: u16, column: u16, - tab_width: usize, + fmt: TextFormat, + annotations: &TextAnnotations, + ignore_virtual_text: bool, ) -> Option<usize> { - let text = doc.text().slice(..); let inner = self.inner_area(doc); // 1 for status if row < inner.top() || row >= inner.bottom() { @@ -318,27 +426,80 @@ impl View { return None; } - let text_row = (row - inner.y) as usize + self.offset.row; - if text_row > text.len_lines() - 1 { - return Some(text.len_chars()); - } + self.text_pos_at_visual_coords( + doc, + row - inner.y, + column - inner.x, + fmt, + annotations, + ignore_virtual_text, + ) + } + + pub fn text_pos_at_visual_coords( + &self, + doc: &Document, + row: u16, + column: u16, + text_fmt: TextFormat, + annotations: &TextAnnotations, + ignore_virtual_text: bool, + ) -> Option<usize> { + let text = doc.text().slice(..); - let text_col = (column - inner.x) as usize + self.offset.col; + let text_row = row as usize + self.offset.vertical_offset; + let text_col = column as usize + self.offset.horizontal_offset; - Some(pos_at_visual_coords( + let (char_idx, virt_lines) = char_idx_at_visual_offset( text, - Position { - row: text_row, - col: text_col, - }, - tab_width, - )) + self.offset.anchor, + text_row as isize, + text_col, + &text_fmt, + annotations, + ); + + // if the cursor is on a line with only virtual text return None + if virt_lines != 0 && ignore_virtual_text { + return None; + } + Some(char_idx) } /// Translates a screen position to position in the text document. /// Returns a usize typed position in bounds of the text if found in this view, None if out of view. - pub fn pos_at_screen_coords(&self, doc: &Document, row: u16, column: u16) -> Option<usize> { - self.text_pos_at_screen_coords(doc, row, column, doc.tab_width()) + pub fn pos_at_screen_coords( + &self, + doc: &Document, + row: u16, + column: u16, + ignore_virtual_text: bool, + ) -> Option<usize> { + self.text_pos_at_screen_coords( + doc, + row, + column, + doc.text_format(self.inner_width(doc), None), + &self.text_annotations(doc, None), + ignore_virtual_text, + ) + } + + pub fn pos_at_visual_coords( + &self, + doc: &Document, + row: u16, + column: u16, + ignore_virtual_text: bool, + ) -> Option<usize> { + self.text_pos_at_visual_coords( + doc, + row, + column, + doc.text_format(self.inner_width(doc), None), + &self.text_annotations(doc, None), + ignore_virtual_text, + ) } /// Translates screen coordinates into coordinates on the gutter of the view. @@ -419,7 +580,10 @@ impl View { #[cfg(test)] mod tests { + use std::sync::Arc; + use super::*; + use arc_swap::ArcSwap; use helix_core::Rope; // 1 diagnostic + 1 spacer + 3 linenr (< 1000 lines) + 1 spacer + 1 diff @@ -429,52 +593,174 @@ mod tests { const DEFAULT_GUTTER_OFFSET_ONLY_DIAGNOSTICS: u16 = 3; use crate::document::Document; - use crate::editor::{GutterConfig, GutterLineNumbersConfig, GutterType}; + use crate::editor::{Config, GutterConfig, GutterLineNumbersConfig, GutterType}; #[test] fn test_text_pos_at_screen_coords() { let mut view = View::new(DocumentId::default(), GutterConfig::default()); view.area = Rect::new(40, 40, 40, 40); let rope = Rope::from_str("abc\n\tdef"); - let doc = Document::from(rope, None); + let doc = Document::from( + rope, + None, + Arc::new(ArcSwap::new(Arc::new(Config::default()))), + ); - assert_eq!(view.text_pos_at_screen_coords(&doc, 40, 2, 4), None); + assert_eq!( + view.text_pos_at_screen_coords( + &doc, + 40, + 2, + TextFormat::default(), + &TextAnnotations::default(), + true + ), + None + ); - assert_eq!(view.text_pos_at_screen_coords(&doc, 40, 41, 4), None); + assert_eq!( + view.text_pos_at_screen_coords( + &doc, + 40, + 41, + TextFormat::default(), + &TextAnnotations::default(), + true + ), + None + ); - assert_eq!(view.text_pos_at_screen_coords(&doc, 0, 2, 4), None); + assert_eq!( + view.text_pos_at_screen_coords( + &doc, + 0, + 2, + TextFormat::default(), + &TextAnnotations::default(), + true + ), + None + ); - assert_eq!(view.text_pos_at_screen_coords(&doc, 0, 49, 4), None); + assert_eq!( + view.text_pos_at_screen_coords( + &doc, + 0, + 49, + TextFormat::default(), + &TextAnnotations::default(), + true + ), + None + ); - assert_eq!(view.text_pos_at_screen_coords(&doc, 0, 41, 4), None); + assert_eq!( + view.text_pos_at_screen_coords( + &doc, + 0, + 41, + TextFormat::default(), + &TextAnnotations::default(), + true + ), + None + ); - assert_eq!(view.text_pos_at_screen_coords(&doc, 40, 81, 4), None); + assert_eq!( + view.text_pos_at_screen_coords( + &doc, + 40, + 81, + TextFormat::default(), + &TextAnnotations::default(), + true + ), + None + ); - assert_eq!(view.text_pos_at_screen_coords(&doc, 78, 41, 4), None); + assert_eq!( + view.text_pos_at_screen_coords( + &doc, + 78, + 41, + TextFormat::default(), + &TextAnnotations::default(), + true + ), + None + ); assert_eq!( - view.text_pos_at_screen_coords(&doc, 40, 40 + DEFAULT_GUTTER_OFFSET + 3, 4), + view.text_pos_at_screen_coords( + &doc, + 40, + 40 + DEFAULT_GUTTER_OFFSET + 3, + TextFormat::default(), + &TextAnnotations::default(), + true + ), Some(3) ); - assert_eq!(view.text_pos_at_screen_coords(&doc, 40, 80, 4), Some(3)); + assert_eq!( + view.text_pos_at_screen_coords( + &doc, + 40, + 80, + TextFormat::default(), + &TextAnnotations::default(), + true + ), + Some(3) + ); assert_eq!( - view.text_pos_at_screen_coords(&doc, 41, 40 + DEFAULT_GUTTER_OFFSET + 1, 4), + view.text_pos_at_screen_coords( + &doc, + 41, + 40 + DEFAULT_GUTTER_OFFSET + 1, + TextFormat::default(), + &TextAnnotations::default(), + true + ), Some(4) ); assert_eq!( - view.text_pos_at_screen_coords(&doc, 41, 40 + DEFAULT_GUTTER_OFFSET + 4, 4), + view.text_pos_at_screen_coords( + &doc, + 41, + 40 + DEFAULT_GUTTER_OFFSET + 4, + TextFormat::default(), + &TextAnnotations::default(), + true + ), Some(5) ); assert_eq!( - view.text_pos_at_screen_coords(&doc, 41, 40 + DEFAULT_GUTTER_OFFSET + 7, 4), + view.text_pos_at_screen_coords( + &doc, + 41, + 40 + DEFAULT_GUTTER_OFFSET + 7, + TextFormat::default(), + &TextAnnotations::default(), + true + ), Some(8) ); - assert_eq!(view.text_pos_at_screen_coords(&doc, 41, 80, 4), Some(8)); + assert_eq!( + view.text_pos_at_screen_coords( + &doc, + 41, + 80, + TextFormat::default(), + &TextAnnotations::default(), + true + ), + Some(8) + ); } #[test] @@ -488,13 +774,19 @@ mod tests { ); view.area = Rect::new(40, 40, 40, 40); let rope = Rope::from_str("abc\n\tdef"); - let doc = Document::from(rope, None); + let doc = Document::from( + rope, + None, + Arc::new(ArcSwap::new(Arc::new(Config::default()))), + ); assert_eq!( view.text_pos_at_screen_coords( &doc, 41, 40 + DEFAULT_GUTTER_OFFSET_ONLY_DIAGNOSTICS + 1, - 4 + TextFormat::default(), + &TextAnnotations::default(), + true ), Some(4) ); @@ -511,8 +803,22 @@ mod tests { ); view.area = Rect::new(40, 40, 40, 40); let rope = Rope::from_str("abc\n\tdef"); - let doc = Document::from(rope, None); - assert_eq!(view.text_pos_at_screen_coords(&doc, 41, 40 + 1, 4), Some(4)); + let doc = Document::from( + rope, + None, + Arc::new(ArcSwap::new(Arc::new(Config::default()))), + ); + assert_eq!( + view.text_pos_at_screen_coords( + &doc, + 41, + 40 + 1, + TextFormat::default(), + &TextAnnotations::default(), + true + ), + Some(4) + ); } #[test] @@ -520,34 +826,80 @@ mod tests { let mut view = View::new(DocumentId::default(), GutterConfig::default()); view.area = Rect::new(40, 40, 40, 40); let rope = Rope::from_str("Hi! こんにちは皆さん"); - let doc = Document::from(rope, None); + let doc = Document::from( + rope, + None, + Arc::new(ArcSwap::new(Arc::new(Config::default()))), + ); assert_eq!( - view.text_pos_at_screen_coords(&doc, 40, 40 + DEFAULT_GUTTER_OFFSET, 4), + view.text_pos_at_screen_coords( + &doc, + 40, + 40 + DEFAULT_GUTTER_OFFSET, + TextFormat::default(), + &TextAnnotations::default(), + true + ), Some(0) ); assert_eq!( - view.text_pos_at_screen_coords(&doc, 40, 40 + DEFAULT_GUTTER_OFFSET + 4, 4), + view.text_pos_at_screen_coords( + &doc, + 40, + 40 + DEFAULT_GUTTER_OFFSET + 4, + TextFormat::default(), + &TextAnnotations::default(), + true + ), Some(4) ); assert_eq!( - view.text_pos_at_screen_coords(&doc, 40, 40 + DEFAULT_GUTTER_OFFSET + 5, 4), + view.text_pos_at_screen_coords( + &doc, + 40, + 40 + DEFAULT_GUTTER_OFFSET + 5, + TextFormat::default(), + &TextAnnotations::default(), + true + ), Some(4) ); assert_eq!( - view.text_pos_at_screen_coords(&doc, 40, 40 + DEFAULT_GUTTER_OFFSET + 6, 4), + view.text_pos_at_screen_coords( + &doc, + 40, + 40 + DEFAULT_GUTTER_OFFSET + 6, + TextFormat::default(), + &TextAnnotations::default(), + true + ), Some(5) ); assert_eq!( - view.text_pos_at_screen_coords(&doc, 40, 40 + DEFAULT_GUTTER_OFFSET + 7, 4), + view.text_pos_at_screen_coords( + &doc, + 40, + 40 + DEFAULT_GUTTER_OFFSET + 7, + TextFormat::default(), + &TextAnnotations::default(), + true + ), Some(5) ); assert_eq!( - view.text_pos_at_screen_coords(&doc, 40, 40 + DEFAULT_GUTTER_OFFSET + 8, 4), + view.text_pos_at_screen_coords( + &doc, + 40, + 40 + DEFAULT_GUTTER_OFFSET + 8, + TextFormat::default(), + &TextAnnotations::default(), + true + ), Some(6) ); } @@ -557,30 +909,69 @@ mod tests { let mut view = View::new(DocumentId::default(), GutterConfig::default()); view.area = Rect::new(40, 40, 40, 40); let rope = Rope::from_str("Hèl̀l̀ò world!"); - let doc = Document::from(rope, None); + let doc = Document::from( + rope, + None, + Arc::new(ArcSwap::new(Arc::new(Config::default()))), + ); assert_eq!( - view.text_pos_at_screen_coords(&doc, 40, 40 + DEFAULT_GUTTER_OFFSET, 4), + view.text_pos_at_screen_coords( + &doc, + 40, + 40 + DEFAULT_GUTTER_OFFSET, + TextFormat::default(), + &TextAnnotations::default(), + true + ), Some(0) ); assert_eq!( - view.text_pos_at_screen_coords(&doc, 40, 40 + DEFAULT_GUTTER_OFFSET + 1, 4), + view.text_pos_at_screen_coords( + &doc, + 40, + 40 + DEFAULT_GUTTER_OFFSET + 1, + TextFormat::default(), + &TextAnnotations::default(), + true + ), Some(1) ); assert_eq!( - view.text_pos_at_screen_coords(&doc, 40, 40 + DEFAULT_GUTTER_OFFSET + 2, 4), + view.text_pos_at_screen_coords( + &doc, + 40, + 40 + DEFAULT_GUTTER_OFFSET + 2, + TextFormat::default(), + &TextAnnotations::default(), + true + ), Some(3) ); assert_eq!( - view.text_pos_at_screen_coords(&doc, 40, 40 + DEFAULT_GUTTER_OFFSET + 3, 4), + view.text_pos_at_screen_coords( + &doc, + 40, + 40 + DEFAULT_GUTTER_OFFSET + 3, + TextFormat::default(), + &TextAnnotations::default(), + true + ), Some(5) ); assert_eq!( - view.text_pos_at_screen_coords(&doc, 40, 40 + DEFAULT_GUTTER_OFFSET + 4, 4), + view.text_pos_at_screen_coords( + &doc, + 40, + 40 + DEFAULT_GUTTER_OFFSET + 4, + TextFormat::default(), + &TextAnnotations::default(), + true + ), Some(7) ); } |