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.rs | 252 |
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( |