aboutsummaryrefslogtreecommitdiff
path: root/helix-view
diff options
context:
space:
mode:
authorPascal Kuthe2023-01-31 17:03:19 +0000
committerGitHub2023-01-31 17:03:19 +0000
commit4dcf1fe66ba30a78edc054780d9b65c2f826530f (patch)
treeffb84ea94f07ceb52494a955b1bd78f115395dc0 /helix-view
parent4eca4b3079bf53de874959270d0b3471d320debc (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.rs74
-rw-r--r--helix-view/src/editor.rs85
-rw-r--r--helix-view/src/gutter.rs293
-rw-r--r--helix-view/src/lib.rs23
-rw-r--r--helix-view/src/theme.rs15
-rw-r--r--helix-view/src/view.rs647
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)
);
}