Unnamed repository; edit this file 'description' to name the repository.
Diffstat (limited to 'helix-view/src/view.rs')
-rw-r--r--helix-view/src/view.rs252
1 files changed, 88 insertions, 164 deletions
diff --git a/helix-view/src/view.rs b/helix-view/src/view.rs
index aecf09a6..ee6fc127 100644
--- a/helix-view/src/view.rs
+++ b/helix-view/src/view.rs
@@ -1,16 +1,15 @@
use crate::{
align_view,
- annotations::diagnostics::InlineDiagnostics,
- document::{DocumentColorSwatches, DocumentInlayHints},
+ document::DocumentInlayHints,
editor::{GutterConfig, GutterType},
graphics::Rect,
- handlers::diagnostics::DiagnosticsHandler,
Align, Document, DocumentId, Theme, ViewId,
};
use helix_core::{
char_idx_at_visual_offset,
doc_formatter::TextFormat,
+ syntax::Highlight,
text_annotations::TextAnnotations,
visual_offset_from_anchor, visual_offset_from_block, Position, RopeSlice, Selection,
Transaction,
@@ -20,6 +19,7 @@ use helix_core::{
use std::{
collections::{HashMap, VecDeque},
fmt,
+ rc::Rc,
};
const JUMP_LIST_CAPACITY: usize = 30;
@@ -39,25 +39,18 @@ impl JumpList {
Self { jumps, current: 0 }
}
- fn push_impl(&mut self, jump: Jump) -> usize {
- let mut num_removed_from_front = 0;
+ pub fn push(&mut self, jump: Jump) {
self.jumps.truncate(self.current);
// don't push duplicates
if self.jumps.back() != Some(&jump) {
// If the jumplist is full, drop the oldest item.
while self.jumps.len() >= JUMP_LIST_CAPACITY {
self.jumps.pop_front();
- num_removed_from_front += 1;
}
self.jumps.push_back(jump);
self.current = self.jumps.len();
}
- num_removed_from_front
- }
-
- pub fn push(&mut self, jump: Jump) {
- self.push_impl(jump);
}
pub fn forward(&mut self, count: usize) -> Option<&Jump> {
@@ -71,22 +64,13 @@ impl JumpList {
// Taking view and doc to prevent unnecessary cloning when jump is not required.
pub fn backward(&mut self, view_id: ViewId, doc: &mut Document, count: usize) -> Option<&Jump> {
- if let Some(mut current) = self.current.checked_sub(count) {
+ if let Some(current) = self.current.checked_sub(count) {
if self.current == self.jumps.len() {
let jump = (doc.id(), doc.selection(view_id).clone());
- let num_removed = self.push_impl(jump);
- current = current.saturating_sub(num_removed);
+ self.push(jump);
}
self.current = current;
-
- // Avoid jumping to the current location.
- let jump @ (doc_id, selection) = self.jumps.get(self.current)?;
- if doc.id() == *doc_id && doc.selection(view_id) == selection {
- self.current = self.current.checked_sub(1)?;
- self.jumps.get(self.current)
- } else {
- Some(jump)
- }
+ self.jumps.get(self.current)
} else {
None
}
@@ -96,7 +80,7 @@ impl JumpList {
self.jumps.retain(|(other_id, _)| other_id != doc_id);
}
- pub fn iter(&self) -> impl DoubleEndedIterator<Item = &Jump> {
+ pub fn iter(&self) -> impl Iterator<Item = &Jump> {
self.jumps.iter()
}
@@ -127,6 +111,7 @@ pub struct ViewPosition {
#[derive(Clone)]
pub struct View {
pub id: ViewId,
+ pub offset: ViewPosition,
pub area: Rect,
pub doc: DocumentId,
pub jumps: JumpList,
@@ -146,14 +131,6 @@ pub struct View {
/// mapping keeps track of the last applied history revision so that only new changes
/// are applied.
doc_revisions: HashMap<DocumentId, usize>,
- // HACKS: there should really only be a global diagnostics handler (the
- // non-focused views should just not have different handling for the cursor
- // line). For that we would need accces to editor everywhere (we want to use
- // the positioning code) so this can only happen by refactoring View and
- // Document into entity component like structure. That is a huge refactor
- // left to future work. For now we treat all views as focused and give them
- // each their own handler.
- pub diagnostics_handler: DiagnosticsHandler,
}
impl fmt::Debug for View {
@@ -171,6 +148,11 @@ impl View {
Self {
id: ViewId::default(),
doc,
+ 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(),
@@ -178,7 +160,6 @@ impl View {
object_selections: Vec::new(),
gutters,
doc_revisions: HashMap::new(),
- diagnostics_handler: DiagnosticsHandler::new(),
}
}
@@ -206,17 +187,11 @@ impl View {
}
pub fn gutter_offset(&self, doc: &Document) -> u16 {
- let total_width = self
- .gutters
+ self.gutters
.layout
.iter()
.map(|gutter| gutter.width(self, doc) as u16)
- .sum();
- if total_width < self.area.width {
- total_width
- } else {
- 0
- }
+ .sum()
}
//
@@ -233,34 +208,23 @@ impl View {
doc: &Document,
scrolloff: usize,
) -> Option<ViewPosition> {
- let view_offset = doc.get_view_offset(self.id)?;
let doc_text = doc.text().slice(..);
let viewport = self.inner_area(doc);
- let vertical_viewport_end = view_offset.vertical_offset + viewport.height as usize;
+ 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);
- let (scrolloff_top, scrolloff_bottom) = if CENTERING {
- (0, 0)
- } else {
- (
- // - 1 from the top so we have at least one gap in the middle.
- scrolloff.min(viewport.height.saturating_sub(1) as usize / 2),
- scrolloff.min(viewport.height as usize / 2),
- )
- };
- let (scrolloff_left, scrolloff_right) = if CENTERING {
- (0, 0)
+ // - 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 = if CENTERING {
+ 0
} else {
- (
- // - 1 from the left so we have at least one gap in the middle.
- scrolloff.min(viewport.width.saturating_sub(1) as usize / 2),
- scrolloff.min(viewport.width as usize / 2),
- )
+ scrolloff.min(viewport.height.saturating_sub(1) as usize / 2)
};
let cursor = doc.selection(self.id).primary().cursor(doc_text);
- let mut offset = view_offset;
+ let mut offset = self.offset;
let off = visual_offset_from_anchor(
doc_text,
offset.anchor,
@@ -271,14 +235,14 @@ impl View {
);
let (new_anchor, at_top) = match off {
- Ok((visual_pos, _)) if visual_pos.row < scrolloff_top + offset.vertical_offset => {
+ Ok((visual_pos, _)) if visual_pos.row < scrolloff + offset.vertical_offset => {
if CENTERING {
// cursor out of view
return None;
}
(true, true)
}
- Ok((visual_pos, _)) if visual_pos.row + scrolloff_bottom >= vertical_viewport_end => {
+ Ok((visual_pos, _)) if visual_pos.row + scrolloff >= vertical_viewport_end => {
(true, false)
}
Ok((_, _)) => (false, false),
@@ -289,9 +253,9 @@ impl View {
if new_anchor {
let v_off = if at_top {
- scrolloff_top as isize
+ scrolloff as isize
} else {
- viewport.height as isize - scrolloff_bottom as isize - 1
+ viewport.height as isize - scrolloff as isize - 1
};
(offset.anchor, offset.vertical_offset) =
char_idx_at_visual_offset(doc_text, cursor, -v_off, 0, &text_fmt, &annotations);
@@ -315,32 +279,32 @@ impl View {
.col;
let last_col = offset.horizontal_offset + viewport.width.saturating_sub(1) as usize;
- if col > last_col.saturating_sub(scrolloff_right) {
+ if col > last_col.saturating_sub(scrolloff) {
// scroll right
- offset.horizontal_offset += col - (last_col.saturating_sub(scrolloff_right))
- } else if col < offset.horizontal_offset + scrolloff_left {
+ offset.horizontal_offset += col - (last_col.saturating_sub(scrolloff))
+ } else if col < offset.horizontal_offset + scrolloff {
// scroll left
- offset.horizontal_offset = col.saturating_sub(scrolloff_left)
+ offset.horizontal_offset = col.saturating_sub(scrolloff)
};
}
// if we are not centering return None if view position is unchanged
- if !CENTERING && offset == view_offset {
+ if !CENTERING && offset == self.offset {
return None;
}
Some(offset)
}
- pub fn ensure_cursor_in_view(&self, doc: &mut Document, scrolloff: usize) {
+ pub fn ensure_cursor_in_view(&mut self, doc: &Document, scrolloff: usize) {
if let Some(offset) = self.offset_coords_to_in_view_center::<false>(doc, scrolloff) {
- doc.set_view_offset(self.id, offset);
+ self.offset = offset;
}
}
- pub fn ensure_cursor_in_view_center(&self, doc: &mut Document, scrolloff: usize) {
+ pub fn ensure_cursor_in_view_center(&mut self, doc: &Document, scrolloff: usize) {
if let Some(offset) = self.offset_coords_to_in_view_center::<true>(doc, scrolloff) {
- doc.set_view_offset(self.id, offset);
+ self.offset = offset;
} else {
align_view(doc, self, Align::Center);
}
@@ -358,7 +322,7 @@ impl View {
#[inline]
pub fn estimate_last_doc_line(&self, doc: &Document) -> usize {
let doc_text = doc.text().slice(..);
- let line = doc_text.char_to_line(doc.view_offset(self.id).anchor.min(doc_text.len_chars()));
+ 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())
@@ -372,10 +336,9 @@ impl View {
let viewport = self.inner_area(doc);
let text_fmt = doc.text_format(viewport.width, None);
let annotations = self.text_annotations(doc, None);
- let view_offset = doc.view_offset(self.id);
// last visual line in view is trivial to compute
- let visual_height = doc.view_offset(self.id).vertical_offset + viewport.height as usize;
+ 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 {
@@ -385,7 +348,7 @@ impl View {
// translate to document line
let pos = visual_offset_from_anchor(
doc_text,
- view_offset.anchor,
+ self.offset.anchor,
usize::MAX,
&text_fmt,
&annotations,
@@ -393,7 +356,7 @@ impl View {
);
match pos {
- Ok((Position { row, .. }, _)) => row.saturating_sub(view_offset.vertical_offset),
+ Ok((Position { row, .. }, _)) => row.saturating_sub(self.offset.vertical_offset),
Err(PosAfterMaxRow) => visual_height.saturating_sub(1),
Err(PosBeforeAnchorRow) => 0,
}
@@ -408,7 +371,10 @@ impl View {
text: RopeSlice,
pos: usize,
) -> Option<Position> {
- let view_offset = doc.view_offset(self.id);
+ if pos < self.offset.anchor {
+ // Line is not visible on screen
+ return None;
+ }
let viewport = self.inner_area(doc);
let text_fmt = doc.text_format(viewport.width, None);
@@ -416,7 +382,7 @@ impl View {
let mut pos = visual_offset_from_anchor(
text,
- view_offset.anchor,
+ self.offset.anchor,
pos,
&text_fmt,
&annotations,
@@ -424,91 +390,60 @@ impl View {
)
.ok()?
.0;
- if pos.row < view_offset.vertical_offset {
+ if pos.row < self.offset.vertical_offset {
return None;
}
- pos.row -= view_offset.vertical_offset;
+ pos.row -= self.offset.vertical_offset;
if pos.row >= viewport.height as usize {
return None;
}
- pos.col = pos.col.saturating_sub(view_offset.horizontal_offset);
+ pos.col = pos.col.saturating_sub(self.offset.horizontal_offset);
Some(pos)
}
/// Get the text annotations to display in the current view for the given document and theme.
- pub fn text_annotations<'a>(
- &self,
- doc: &'a Document,
- theme: Option<&Theme>,
- ) -> TextAnnotations<'a> {
- let mut text_annotations = TextAnnotations::default();
-
- if let Some(labels) = doc.jump_labels.get(&self.id) {
- let style = theme.and_then(|t| t.find_highlight("ui.virtual.jump-label"));
- text_annotations.add_overlay(labels, style);
- }
+ pub fn text_annotations(&self, doc: &Document, theme: Option<&Theme>) -> TextAnnotations {
+ // TODO custom annotations for custom views like side by side diffs
+
+ let mut text_annotations = doc.text_annotations(theme);
- if let Some(DocumentInlayHints {
+ let DocumentInlayHints {
id: _,
type_inlay_hints,
parameter_inlay_hints,
other_inlay_hints,
padding_before_inlay_hints,
padding_after_inlay_hints,
- }) = doc.inlay_hints.get(&self.id)
- {
- let type_style = theme.and_then(|t| t.find_highlight("ui.virtual.inlay-hint.type"));
- let parameter_style =
- theme.and_then(|t| t.find_highlight("ui.virtual.inlay-hint.parameter"));
- let other_style = theme.and_then(|t| t.find_highlight("ui.virtual.inlay-hint"));
-
- // Overlapping annotations are ignored apart from the first so the order here is not random:
- // types -> parameters -> others should hopefully be the "correct" order for most use cases,
- // with the padding coming before and after as expected.
- text_annotations
- .add_inline_annotations(padding_before_inlay_hints, None)
- .add_inline_annotations(type_inlay_hints, type_style)
- .add_inline_annotations(parameter_inlay_hints, parameter_style)
- .add_inline_annotations(other_inlay_hints, other_style)
- .add_inline_annotations(padding_after_inlay_hints, None);
+ } = match doc.inlay_hints.get(&self.id) {
+ Some(doc_inlay_hints) => doc_inlay_hints,
+ None => return text_annotations,
};
- let config = doc.config.load();
-
- if config.lsp.display_color_swatches {
- if let Some(DocumentColorSwatches {
- color_swatches,
- colors,
- color_swatches_padding,
- }) = &doc.color_swatches
- {
- for (color_swatch, color) in color_swatches.iter().zip(colors) {
- text_annotations
- .add_inline_annotations(std::slice::from_ref(color_swatch), Some(*color));
- }
- text_annotations.add_inline_annotations(color_swatches_padding, None);
+ let type_style = theme
+ .and_then(|t| t.find_scope_index("ui.virtual.inlay-hint.type"))
+ .map(Highlight);
+ let parameter_style = theme
+ .and_then(|t| t.find_scope_index("ui.virtual.inlay-hint.parameter"))
+ .map(Highlight);
+ let other_style = theme
+ .and_then(|t| t.find_scope_index("ui.virtual.inlay-hint"))
+ .map(Highlight);
+
+ let mut add_annotations = |annotations: &Rc<[_]>, style| {
+ if !annotations.is_empty() {
+ text_annotations.add_inline_annotations(Rc::clone(annotations), style);
}
- }
+ };
- let width = self.inner_width(doc);
- let enable_cursor_line = self
- .diagnostics_handler
- .show_cursorline_diagnostics(doc, self.id);
- let config = config.inline_diagnostics.prepare(width, enable_cursor_line);
- if !config.disabled() {
- let cursor = doc
- .selection(self.id)
- .primary()
- .cursor(doc.text().slice(..));
- text_annotations.add_line_annotation(InlineDiagnostics::new(
- doc,
- cursor,
- width,
- doc.view_offset(self.id).horizontal_offset,
- config,
- ));
- }
+ // Overlapping annotations are ignored apart from the first so the order here is not random:
+ // types -> parameters -> others should hopefully be the "correct" order for most use cases,
+ // with the padding coming before and after as expected.
+ add_annotations(padding_before_inlay_hints, None);
+ add_annotations(type_inlay_hints, type_style);
+ add_annotations(parameter_inlay_hints, parameter_style);
+ add_annotations(other_inlay_hints, other_style);
+ add_annotations(padding_after_inlay_hints, None);
text_annotations
}
@@ -552,14 +487,13 @@ impl View {
ignore_virtual_text: bool,
) -> Option<usize> {
let text = doc.text().slice(..);
- let view_offset = doc.view_offset(self.id);
- let text_row = row as usize + view_offset.vertical_offset;
- let text_col = column as usize + view_offset.horizontal_offset;
+ let text_row = row as usize + self.offset.vertical_offset;
+ let text_col = column as usize + self.offset.horizontal_offset;
let (char_idx, virt_lines) = char_idx_at_visual_offset(
text,
- view_offset.anchor,
+ self.offset.anchor,
text_row as isize,
text_col,
&text_fmt,
@@ -691,7 +625,7 @@ mod tests {
use super::*;
use arc_swap::ArcSwap;
- use helix_core::{syntax, Rope};
+ use helix_core::Rope;
// 1 diagnostic + 1 spacer + 3 linenr (< 1000 lines) + 1 spacer + 1 diff
const DEFAULT_GUTTER_OFFSET: u16 = 7;
@@ -707,13 +641,11 @@ mod tests {
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 mut doc = Document::from(
+ let doc = Document::from(
rope,
None,
Arc::new(ArcSwap::new(Arc::new(Config::default()))),
- Arc::new(ArcSwap::from_pointee(syntax::Loader::default())),
);
- doc.ensure_view_init(view.id);
assert_eq!(
view.text_pos_at_screen_coords(
@@ -883,13 +815,11 @@ mod tests {
);
view.area = Rect::new(40, 40, 40, 40);
let rope = Rope::from_str("abc\n\tdef");
- let mut doc = Document::from(
+ let doc = Document::from(
rope,
None,
Arc::new(ArcSwap::new(Arc::new(Config::default()))),
- Arc::new(ArcSwap::from_pointee(syntax::Loader::default())),
);
- doc.ensure_view_init(view.id);
assert_eq!(
view.text_pos_at_screen_coords(
&doc,
@@ -914,13 +844,11 @@ mod tests {
);
view.area = Rect::new(40, 40, 40, 40);
let rope = Rope::from_str("abc\n\tdef");
- let mut doc = Document::from(
+ let doc = Document::from(
rope,
None,
Arc::new(ArcSwap::new(Arc::new(Config::default()))),
- Arc::new(ArcSwap::from_pointee(syntax::Loader::default())),
);
- doc.ensure_view_init(view.id);
assert_eq!(
view.text_pos_at_screen_coords(
&doc,
@@ -939,13 +867,11 @@ 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 mut doc = Document::from(
+ let doc = Document::from(
rope,
None,
Arc::new(ArcSwap::new(Arc::new(Config::default()))),
- Arc::new(ArcSwap::from_pointee(syntax::Loader::default())),
);
- doc.ensure_view_init(view.id);
assert_eq!(
view.text_pos_at_screen_coords(
@@ -1024,13 +950,11 @@ 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 mut doc = Document::from(
+ let doc = Document::from(
rope,
None,
Arc::new(ArcSwap::new(Arc::new(Config::default()))),
- Arc::new(ArcSwap::from_pointee(syntax::Loader::default())),
);
- doc.ensure_view_init(view.id);
assert_eq!(
view.text_pos_at_screen_coords(