From 557fd86e71062a1de83d0011f8208cf2fce0dd5f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= <blaz@mxxn.io>
Date: Thu, 19 Aug 2021 13:19:15 +0900
Subject: [PATCH] Extract view.inner_area(), simplify
 render_focused_view_elements

---
 helix-term/src/commands.rs      |  26 +++---
 helix-term/src/ui/completion.rs |   2 +-
 helix-term/src/ui/editor.rs     | 154 ++++++++++++--------------------
 helix-view/src/editor.rs        |   8 +-
 helix-view/src/view.rs          |  41 +++++----
 5 files changed, 97 insertions(+), 134 deletions(-)

diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs
index b2792720..d3c5dd76 100644
--- a/helix-term/src/commands.rs
+++ b/helix-term/src/commands.rs
@@ -110,7 +110,7 @@ fn align_view(doc: &Document, view: &mut View, align: Align) {
         .cursor(doc.text().slice(..));
     let line = doc.text().char_to_line(pos);
 
-    let height = view.area.height.saturating_sub(1) as usize; // -1 for statusline
+    let height = view.inner_area().height as usize;
 
     let relative = match align {
         Align::Center => height / 2,
@@ -455,7 +455,7 @@ fn goto_first_nonwhitespace(cx: &mut Context) {
 fn goto_window(cx: &mut Context, align: Align) {
     let (view, doc) = current!(cx.editor);
 
-    let height = view.area.height.saturating_sub(1) as usize; // -1 for statusline
+    let height = view.inner_area().height as 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
@@ -898,11 +898,9 @@ pub fn scroll(cx: &mut Context, offset: usize, direction: Direction) {
         return;
     }
 
-    let scrolloff = cx
-        .editor
-        .config
-        .scrolloff
-        .min(view.area.height as usize / 2);
+    let height = view.inner_area().height;
+
+    let scrolloff = cx.editor.config.scrolloff.min(height as usize / 2);
 
     view.offset.row = match direction {
         Forward => view.offset.row + offset,
@@ -928,25 +926,25 @@ pub fn scroll(cx: &mut Context, offset: usize, direction: Direction) {
 
 fn page_up(cx: &mut Context) {
     let view = view!(cx.editor);
-    let offset = view.area.height as usize;
+    let offset = view.inner_area().height as usize;
     scroll(cx, offset, Direction::Backward);
 }
 
 fn page_down(cx: &mut Context) {
     let view = view!(cx.editor);
-    let offset = view.area.height as usize;
+    let offset = view.inner_area().height as usize;
     scroll(cx, offset, Direction::Forward);
 }
 
 fn half_page_up(cx: &mut Context) {
     let view = view!(cx.editor);
-    let offset = view.area.height as usize / 2;
+    let offset = view.inner_area().height as usize / 2;
     scroll(cx, offset, Direction::Backward);
 }
 
 fn half_page_down(cx: &mut Context) {
     let view = view!(cx.editor);
-    let offset = view.area.height as usize / 2;
+    let offset = view.inner_area().height as usize / 2;
     scroll(cx, offset, Direction::Forward);
 }
 
@@ -4113,9 +4111,9 @@ fn align_view_middle(cx: &mut Context) {
         .cursor(doc.text().slice(..));
     let pos = coords_at_pos(doc.text().slice(..), pos);
 
-    view.offset.col = pos.col.saturating_sub(
-        ((view.area.width as usize).saturating_sub(crate::ui::editor::GUTTER_OFFSET as usize)) / 2,
-    );
+    view.offset.col = pos
+        .col
+        .saturating_sub((view.inner_area().width as usize) / 2);
 }
 
 fn scroll_up(cx: &mut Context) {
diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs
index 985d4e48..90657764 100644
--- a/helix-term/src/ui/completion.rs
+++ b/helix-term/src/ui/completion.rs
@@ -314,7 +314,7 @@ impl Component for Completion {
             let half = area.height / 2;
             let height = 15.min(half);
             // we want to make sure the cursor is visible (not hidden behind the documentation)
-            let y = if cursor_pos + view.area.y
+            let y = if cursor_pos + area.y
                 >= (cx.editor.tree.area().height - height - 2/* statusline + commandline */)
             {
                 0
diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs
index 98462e26..21e6cd9b 100644
--- a/helix-term/src/ui/editor.rs
+++ b/helix-term/src/ui/editor.rs
@@ -38,8 +38,6 @@ pub struct EditorView {
     autoinfo: Option<Info>,
 }
 
-pub const GUTTER_OFFSET: u16 = 7; // 1 diagnostic + 5 linenr + 1 gutter
-
 impl Default for EditorView {
     fn default() -> Self {
         Self::new(Keymaps::default())
@@ -74,15 +72,10 @@ impl EditorView {
         loader: &syntax::Loader,
         config: &helix_view::editor::Config,
     ) {
-        let area = Rect::new(
-            view.area.x + GUTTER_OFFSET,
-            view.area.y,
-            view.area.width - GUTTER_OFFSET,
-            view.area.height.saturating_sub(1),
-        ); // - 1 for statusline
-        let height = view.area.height.saturating_sub(1); // - 1 for statusline
+        let inner = view.inner_area();
+        let area = view.area;
 
-        let highlights = Self::doc_syntax_highlights(doc, view.offset, height, theme, loader);
+        let highlights = Self::doc_syntax_highlights(doc, view.offset, inner.height, theme, loader);
         let highlights = syntax::merge(highlights, Self::doc_diagnostics_highlights(doc, theme));
         let highlights: Box<dyn Iterator<Item = HighlightEvent>> = if is_focused {
             Box::new(syntax::merge(
@@ -93,11 +86,11 @@ impl EditorView {
             Box::new(highlights)
         };
 
-        Self::render_text_highlights(doc, view.offset, area, surface, theme, highlights);
-        Self::render_gutter(doc, view, area, surface, theme, config);
+        Self::render_text_highlights(doc, view.offset, inner, surface, theme, highlights);
+        Self::render_gutter(doc, view, view.area, surface, theme, is_focused, config);
 
         if is_focused {
-            Self::render_focused_view_elements(view, doc, area, theme, surface);
+            Self::render_focused_view_elements(view, doc, inner, theme, surface);
         }
 
         // if we're not at the edge of the screen, draw a right border
@@ -113,7 +106,7 @@ impl EditorView {
             }
         }
 
-        self.render_diagnostics(doc, view, area, surface, theme);
+        self.render_diagnostics(doc, view, inner, surface, theme);
 
         let area = Rect::new(
             view.area.x,
@@ -369,7 +362,7 @@ impl EditorView {
         }
     }
 
-    /// Render brace match, selected line numbers, etc (meant for the focused view only)
+    /// Render brace match, etc (meant for the focused view only)
     pub fn render_focused_view_elements(
         view: &View,
         doc: &Document,
@@ -377,77 +370,29 @@ impl EditorView {
         theme: &Theme,
         surface: &mut Surface,
     ) {
-        let text = doc.text().slice(..);
-        let selection = doc.selection(view.id);
-        let last_line = view.last_line(doc);
-        let screen = {
-            let start = text.line_to_char(view.offset.row);
-            let end = text.line_to_char(last_line + 1) + 1; // +1 for cursor at end of text.
-            Range::new(start, end)
-        };
+        // Highlight matching braces
+        if let Some(syntax) = doc.syntax() {
+            let text = doc.text().slice(..);
+            use helix_core::match_brackets;
+            let pos = doc.selection(view.id).primary().cursor(text);
 
-        // render selected linenr(s)
-        let linenr_select: Style = theme
-            .try_get("ui.linenr.selected")
-            .unwrap_or_else(|| theme.get("ui.linenr"));
+            let pos = match_brackets::find(syntax, doc.text(), pos)
+                .and_then(|pos| view.screen_coords_at_pos(doc, text, pos));
 
-        // 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.
-        let draw_last = text.line_to_byte(last_line) < text.len_bytes();
+            if let Some(pos) = pos {
+                // ensure col is on screen
+                if (pos.col as u16) < viewport.width + view.offset.col as u16
+                    && pos.col >= view.offset.col
+                {
+                    let style = theme.try_get("ui.cursor.match").unwrap_or_else(|| {
+                        Style::default()
+                            .add_modifier(Modifier::REVERSED)
+                            .add_modifier(Modifier::DIM)
+                    });
 
-        for selection in selection.iter().filter(|range| range.overlaps(&screen)) {
-            let head = view.screen_coords_at_pos(
-                doc,
-                text,
-                if selection.head > selection.anchor {
-                    selection.head - 1
-                } else {
-                    selection.head
-                },
-            );
-            if let Some(head) = head {
-                // Highlight line number for selected lines.
-                let line_number = view.offset.row + head.row;
-                let line_number_text = if line_number == last_line && !draw_last {
-                    "    ~".into()
-                } else {
-                    format!("{:>5}", line_number + 1)
-                };
-                surface.set_stringn(
-                    viewport.x - GUTTER_OFFSET + 1,
-                    viewport.y + head.row as u16,
-                    line_number_text,
-                    5,
-                    linenr_select,
-                );
-
-                // Highlight matching braces
-                // TODO: set cursor position for IME
-                if let Some(syntax) = doc.syntax() {
-                    use helix_core::match_brackets;
-                    let pos = doc
-                        .selection(view.id)
-                        .primary()
-                        .cursor(doc.text().slice(..));
-                    let pos = match_brackets::find(syntax, doc.text(), pos)
-                        .and_then(|pos| view.screen_coords_at_pos(doc, text, pos));
-
-                    if let Some(pos) = pos {
-                        // ensure col is on screen
-                        if (pos.col as u16) < viewport.width + view.offset.col as u16
-                            && pos.col >= view.offset.col
-                        {
-                            let style = theme.try_get("ui.cursor.match").unwrap_or_else(|| {
-                                Style::default()
-                                    .add_modifier(Modifier::REVERSED)
-                                    .add_modifier(Modifier::DIM)
-                            });
-
-                            surface
-                                .get_mut(viewport.x + pos.col as u16, viewport.y + pos.row as u16)
-                                .set_style(style);
-                        }
-                    }
+                    surface
+                        .get_mut(viewport.x + pos.col as u16, viewport.y + pos.row as u16)
+                        .set_style(style);
                 }
             }
         }
@@ -460,12 +405,15 @@ impl EditorView {
         viewport: Rect,
         surface: &mut Surface,
         theme: &Theme,
+        is_focused: bool,
         config: &helix_view::editor::Config,
     ) {
         let text = doc.text().slice(..);
         let last_line = view.last_line(doc);
 
         let linenr = theme.get("ui.linenr");
+        let linenr_select: Style = theme.try_get("ui.linenr.selected").unwrap_or(linenr);
+
         let warning = theme.get("warning");
         let error = theme.get("error");
         let info = theme.get("info");
@@ -478,11 +426,21 @@ impl EditorView {
         let current_line = doc
             .text()
             .char_to_line(doc.selection(view.id).primary().anchor);
+
+        // it's used inside an iterator so the collect isn't needless:
+        // https://github.com/rust-lang/rust-clippy/issues/6164
+        #[allow(clippy::clippy::needless_collect)]
+        let cursors: Vec<_> = doc
+            .selection(view.id)
+            .iter()
+            .map(|range| range.cursor_line(text))
+            .collect();
+
         for (i, line) in (view.offset.row..(last_line + 1)).enumerate() {
             use helix_core::diagnostic::Severity;
             if let Some(diagnostic) = doc.diagnostics().iter().find(|d| d.line == line) {
                 surface.set_stringn(
-                    viewport.x - GUTTER_OFFSET,
+                    viewport.x,
                     viewport.y + i as u16,
                     "●",
                     1,
@@ -495,25 +453,27 @@ impl EditorView {
                 );
             }
 
-            // Line numbers having selections are rendered
-            // differently, further below.
-            let line_number_text = if line == last_line && !draw_last {
+            let selected = cursors.contains(&line);
+
+            let text = if line == last_line && !draw_last {
                 "    ~".into()
             } else {
-                match config.line_number {
-                    LineNumber::Absolute => format!("{:>5}", line + 1),
-                    LineNumber::Relative => {
-                        let relative_line = abs_diff(current_line, line);
-                        format!("{:>5}", relative_line)
-                    }
-                }
+                let line = match config.line_number {
+                    LineNumber::Absolute => line + 1,
+                    LineNumber::Relative => abs_diff(current_line, line),
+                };
+                format!("{:>5}", line)
             };
             surface.set_stringn(
-                viewport.x + 1 - GUTTER_OFFSET,
+                viewport.x + 1,
                 viewport.y + i as u16,
-                line_number_text,
+                text,
                 5,
-                linenr,
+                if selected && is_focused {
+                    linenr_select
+                } else {
+                    linenr
+                },
             );
         }
     }
diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs
index 478f3818..ec80580e 100644
--- a/helix-view/src/editor.rs
+++ b/helix-view/src/editor.rs
@@ -194,7 +194,7 @@ impl Editor {
                     .primary()
                     .cursor(doc.text().slice(..));
                 let line = doc.text().char_to_line(pos);
-                view.offset.row = line.saturating_sub(view.area.height as usize / 2);
+                view.offset.row = line.saturating_sub(view.inner_area().height as usize / 2);
 
                 return;
             }
@@ -344,7 +344,6 @@ impl Editor {
     // }
 
     pub fn cursor(&self) -> (Option<Position>, CursorKind) {
-        const OFFSET: u16 = 7; // 1 diagnostic + 5 linenr + 1 gutter
         let view = view!(self);
         let doc = &self.documents[view.doc];
         let cursor = doc
@@ -352,8 +351,9 @@ impl Editor {
             .primary()
             .cursor(doc.text().slice(..));
         if let Some(mut pos) = view.screen_coords_at_pos(doc, doc.text().slice(..), cursor) {
-            pos.col += view.area.x as usize + OFFSET as usize;
-            pos.row += view.area.y as usize;
+            let inner = view.inner_area();
+            pos.col += inner.x as usize;
+            pos.row += inner.y as usize;
             (Some(pos), CursorKind::Hidden)
         } else {
             (None, CursorKind::Hidden)
diff --git a/helix-view/src/view.rs b/helix-view/src/view.rs
index f688dd7f..b707c092 100644
--- a/helix-view/src/view.rs
+++ b/helix-view/src/view.rs
@@ -80,25 +80,32 @@ impl View {
         }
     }
 
+    pub fn inner_area(&self) -> Rect {
+        // TODO: not ideal
+        const OFFSET: u16 = 7; // 1 diagnostic + 5 linenr + 1 gutter
+        Rect::new(
+            self.area.x + OFFSET,
+            self.area.y,
+            self.area.width - OFFSET,
+            self.area.height.saturating_sub(1), // -1 for statusline
+        )
+    }
+
     pub fn ensure_cursor_in_view(&mut self, doc: &Document, scrolloff: usize) {
         let cursor = doc
             .selection(self.id)
             .primary()
             .cursor(doc.text().slice(..));
-        let pos = coords_at_pos(doc.text().slice(..), cursor);
-        let line = pos.row;
-        let col = pos.col;
-        let height = self.area.height.saturating_sub(1); // - 1 for statusline
-        let last_line = (self.offset.row + height as usize).saturating_sub(1);
+        let Position { col, row: line } = coords_at_pos(doc.text().slice(..), cursor);
+        let inner_area = self.inner_area();
+        let last_line = (self.offset.row + inner_area.height as usize).saturating_sub(1);
 
         // - 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(height.saturating_sub(1) as usize / 2);
+        let scrolloff = scrolloff.min(inner_area.height.saturating_sub(1) as usize / 2);
 
-        // TODO: not ideal
-        const OFFSET: usize = 7; // 1 diagnostic + 5 linenr + 1 gutter
-        let last_col = (self.offset.col + self.area.width as usize).saturating_sub(OFFSET + 1);
+        let last_col = self.offset.col + inner_area.width.saturating_sub(1) as usize;
 
         if line > last_line.saturating_sub(scrolloff) {
             // scroll down
@@ -120,7 +127,7 @@ impl View {
     /// Calculates the last visible line on screen
     #[inline]
     pub fn last_line(&self, doc: &Document) -> usize {
-        let height = self.area.height.saturating_sub(1); // - 1 for statusline
+        let height = self.inner_area().height;
         std::cmp::min(
             // Saturating subs to make it inclusive zero indexing.
             (self.offset.row + height as usize).saturating_sub(1),
@@ -172,19 +179,17 @@ impl View {
         column: u16,
         tab_width: usize,
     ) -> Option<usize> {
-        // TODO: not ideal
-        const OFFSET: u16 = 7; // 1 diagnostic + 5 linenr + 1 gutter
-
-        // 2 for status
-        if row < self.area.top() || row > self.area.bottom().saturating_sub(2) {
+        let inner = self.inner_area();
+        // 1 for status
+        if row < inner.top() || row >= inner.bottom() {
             return None;
         }
 
-        if column < self.area.left() + OFFSET || column > self.area.right() {
+        if column < inner.left() || column > inner.right() {
             return None;
         }
 
-        let line_number = (row - self.area.y) as usize + self.offset.row;
+        let line_number = (row - inner.y) as usize + self.offset.row;
 
         if line_number > text.len_lines() - 1 {
             return Some(text.len_chars());
@@ -194,7 +199,7 @@ impl View {
 
         let current_line = text.line(line_number);
 
-        let target = (column - OFFSET - self.area.x) as usize + self.offset.col;
+        let target = (column - inner.x) as usize + self.offset.col;
         let mut selected = 0;
 
         for grapheme in RopeGraphemes::new(current_line) {