From 011f9aa47f2316f120da48d342430c7c5caaf107 Mon Sep 17 00:00:00 2001
From: CossonLeo <20379044+cossonleo@users.noreply.github.com>
Date: Wed, 8 Sep 2021 07:33:59 +0000
Subject: [PATCH] Optimize completion doc position. (#691)

* optimize completion doc's render

* optimize completion doc's render

* optimize completion doc position

* cargo fmt

* fix panic

* use saturating_sub

* fixs

* fix clippy

* limit completion doc max width 120
---
 helix-term/src/ui/completion.rs | 47 +++++++++++++++++++++----------
 helix-term/src/ui/markdown.rs   | 28 +++++++++++++++---
 helix-term/src/ui/popup.rs      | 50 +++++++++++++++++++--------------
 3 files changed, 85 insertions(+), 40 deletions(-)

diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs
index 90657764..6c9e3a80 100644
--- a/helix-term/src/ui/completion.rs
+++ b/helix-term/src/ui/completion.rs
@@ -262,8 +262,7 @@ impl Component for Completion {
                 .cursor(doc.text().slice(..));
             let cursor_pos = (helix_core::coords_at_pos(doc.text().slice(..), cursor_pos).row
                 - view.offset.row) as u16;
-
-            let mut doc = match &option.documentation {
+            let mut markdown_doc = match &option.documentation {
                 Some(lsp::Documentation::String(contents))
                 | Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
                     kind: lsp::MarkupKind::PlainText,
@@ -311,24 +310,42 @@ impl Component for Completion {
                 None => return,
             };
 
-            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 + area.y
-                >= (cx.editor.tree.area().height - height - 2/* statusline + commandline */)
-            {
-                0
-            } else {
-                // -2 to subtract command line + statusline. a bit of a hack, because of splits.
-                area.height.saturating_sub(height).saturating_sub(2)
-            };
+            let (popup_x, popup_y) = self.popup.get_rel_position(area, cx);
+            let (popup_width, _popup_height) = self.popup.get_size();
+            let mut width = area
+                .width
+                .saturating_sub(popup_x)
+                .saturating_sub(popup_width);
+            let area = if width > 30 {
+                let mut height = area.height.saturating_sub(popup_y);
+                let x = popup_x + popup_width;
+                let y = popup_y;
 
-            let area = Rect::new(0, y, area.width, height);
+                if let Some((rel_width, rel_height)) = markdown_doc.required_size((width, height)) {
+                    width = rel_width;
+                    height = rel_height;
+                }
+                Rect::new(x, y, width, height)
+            } else {
+                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 + area.y
+                    >= (cx.editor.tree.area().height - height - 2/* statusline + commandline */)
+                {
+                    0
+                } else {
+                    // -2 to subtract command line + statusline. a bit of a hack, because of splits.
+                    area.height.saturating_sub(height).saturating_sub(2)
+                };
+
+                Rect::new(0, y, area.width, height)
+            };
 
             // clear area
             let background = cx.editor.theme.get("ui.popup");
             surface.clear_with(area, background);
-            doc.render(area, surface, cx);
+            markdown_doc.render(area, surface, cx);
         }
     }
 }
diff --git a/helix-term/src/ui/markdown.rs b/helix-term/src/ui/markdown.rs
index 28542cdc..87b35a2d 100644
--- a/helix-term/src/ui/markdown.rs
+++ b/helix-term/src/ui/markdown.rs
@@ -215,10 +215,30 @@ impl Component for Markdown {
     }
 
     fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> {
-        let contents = parse(&self.contents, None, &self.config_loader);
         let padding = 2;
-        let width = std::cmp::min(contents.width() as u16 + padding, viewport.0);
-        let height = std::cmp::min(contents.height() as u16 + padding, viewport.1);
-        Some((width, height))
+        if padding >= viewport.1 || padding >= viewport.0 {
+            return None;
+        }
+        let contents = parse(&self.contents, None, &self.config_loader);
+        let max_text_width = (viewport.0 - padding).min(120);
+        let mut text_width = 0;
+        let mut height = padding;
+        for content in contents {
+            height += 1;
+            let content_width = content.width() as u16;
+            if content_width > max_text_width {
+                text_width = max_text_width;
+                height += content_width / max_text_width;
+            } else if content_width > text_width {
+                text_width = content_width;
+            }
+
+            if height >= viewport.1 {
+                height = viewport.1;
+                break;
+            }
+        }
+
+        Some((text_width + padding, height))
     }
 }
diff --git a/helix-term/src/ui/popup.rs b/helix-term/src/ui/popup.rs
index e126c845..846cefb8 100644
--- a/helix-term/src/ui/popup.rs
+++ b/helix-term/src/ui/popup.rs
@@ -31,6 +31,33 @@ impl<T: Component> Popup<T> {
         self.position = pos;
     }
 
+    pub fn get_rel_position(&mut self, viewport: Rect, cx: &Context) -> (u16, u16) {
+        let position = self
+            .position
+            .get_or_insert_with(|| cx.editor.cursor().0.unwrap_or_default());
+
+        let (width, height) = self.size;
+
+        // -- make sure frame doesn't stick out of bounds
+        let mut rel_x = position.col as u16;
+        let rel_y = position.row as u16;
+        if viewport.width <= rel_x + width {
+            rel_x = rel_x.saturating_sub((rel_x + width).saturating_sub(viewport.width));
+        };
+
+        // TODO: be able to specify orientation preference. We want above for most popups, below
+        // for menus/autocomplete.
+        if height <= rel_y {
+            (rel_x, rel_y.saturating_sub(height)) // position above point
+        } else {
+            (rel_x, rel_y + 1) // position below point
+        }
+    }
+
+    pub fn get_size(&self) -> (u16, u16) {
+        (self.size.0, self.size.1)
+    }
+
     pub fn scroll(&mut self, offset: usize, direction: bool) {
         if direction {
             self.scroll += offset;
@@ -108,29 +135,10 @@ impl<T: Component> Component for Popup<T> {
     fn render(&mut self, viewport: Rect, surface: &mut Surface, cx: &mut Context) {
         cx.scroll = Some(self.scroll);
 
-        let position = self
-            .position
-            .get_or_insert_with(|| cx.editor.cursor().0.unwrap_or_default());
-
-        let (width, height) = self.size;
-
-        // -- make sure frame doesn't stick out of bounds
-        let mut rel_x = position.col as u16;
-        let mut rel_y = position.row as u16;
-        if viewport.width <= rel_x + width {
-            rel_x = rel_x.saturating_sub((rel_x + width).saturating_sub(viewport.width));
-        };
-
-        // TODO: be able to specify orientation preference. We want above for most popups, below
-        // for menus/autocomplete.
-        if height <= rel_y {
-            rel_y = rel_y.saturating_sub(height) // position above point
-        } else {
-            rel_y += 1 // position below point
-        }
+        let (rel_x, rel_y) = self.get_rel_position(viewport, cx);
 
         // clip to viewport
-        let area = viewport.intersection(Rect::new(rel_x, rel_y, width, height));
+        let area = viewport.intersection(Rect::new(rel_x, rel_y, self.size.0, self.size.1));
 
         // clear area
         let background = cx.editor.theme.get("ui.popup");