From cc357d50964ecd5760f1367a8a7b63fd45a0f1ce Mon Sep 17 00:00:00 2001
From: wojciechkepka <wojtek.kepka@protonmail.com>
Date: Sun, 20 Jun 2021 21:31:45 +0200
Subject: [PATCH] Add progress spinners to status line

---
 helix-lsp/src/lib.rs          |  4 +++
 helix-term/src/application.rs | 51 ++++++++++++++++++++++++++++++-----
 helix-term/src/commands.rs    |  2 +-
 helix-term/src/compositor.rs  |  3 ++-
 helix-term/src/config.rs      | 33 +++++++++--------------
 helix-term/src/ui/editor.rs   | 23 ++++++++++++++--
 6 files changed, 85 insertions(+), 31 deletions(-)

diff --git a/helix-lsp/src/lib.rs b/helix-lsp/src/lib.rs
index 49d5527f..b25a7aca 100644
--- a/helix-lsp/src/lib.rs
+++ b/helix-lsp/src/lib.rs
@@ -347,6 +347,10 @@ impl LspProgressMap {
         self.0.get(&id)
     }
 
+    pub fn is_progressing(&self, id: usize) -> bool {
+        self.0.get(&id).map(|it| !it.is_empty()).unwrap_or_default()
+    }
+
     /// Returns last progress status for a given server with `id` and `token`.
     pub fn progress(&self, id: usize, token: &lsp::ProgressToken) -> Option<&ProgressStatus> {
         self.0.get(&id).and_then(|values| values.get(token))
diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs
index 08853ed0..b67bb73d 100644
--- a/helix-term/src/application.rs
+++ b/helix-term/src/application.rs
@@ -2,11 +2,18 @@ use helix_core::syntax;
 use helix_lsp::{lsp, LspProgressMap};
 use helix_view::{document::Mode, theme, Document, Editor, Theme, View};
 
-use crate::{args::Args, compositor::Compositor, config::Config, keymap::Keymaps, ui};
+use crate::{
+    args::Args,
+    compositor::Compositor,
+    config::Config,
+    keymap::Keymaps,
+    ui::{self, Spinner},
+};
 
 use log::{error, info};
 
 use std::{
+    collections::HashMap,
     future::Future,
     io::{self, stdout, Stdout, Write},
     path::PathBuf,
@@ -42,7 +49,7 @@ pub struct Application {
     callbacks: LspCallbacks,
 
     lsp_progress: LspProgressMap,
-    lsp_progress_enabled: bool,
+    lsp_display_messages: bool,
 }
 
 impl Application {
@@ -62,7 +69,7 @@ impl Application {
             .as_deref()
             .unwrap_or(include_bytes!("../../languages.toml"));
 
-        let theme = if let Some(theme) = &config.global.theme {
+        let theme = if let Some(theme) = &config.theme {
             match theme_loader.load(theme) {
                 Ok(theme) => theme,
                 Err(e) => {
@@ -112,7 +119,7 @@ impl Application {
             syn_loader,
             callbacks: FuturesUnordered::new(),
             lsp_progress: LspProgressMap::new(),
-            lsp_progress_enabled: config.global.lsp_progress,
+            lsp_display_messages: config.lsp.display_messages,
         };
 
         Ok(app)
@@ -305,11 +312,23 @@ impl Application {
                                     (None, message, &None)
                                 } else {
                                     self.lsp_progress.end_progress(server_id, &token);
+                                    if !self.lsp_progress.is_progressing(server_id) {
+                                        let ui = self
+                                            .compositor
+                                            .find(std::any::type_name::<ui::EditorView>())
+                                            .unwrap();
+                                        if let Some(ui) =
+                                            ui.as_any_mut().downcast_mut::<ui::EditorView>()
+                                        {
+                                            ui.spinners_mut().get_or_create(server_id).stop();
+                                        };
+                                    }
                                     self.editor.clear_status();
                                     return;
                                 }
                             }
                         };
+
                         let token_d: &dyn std::fmt::Display = match &token {
                             lsp::NumberOrString::Number(n) => n,
                             lsp::NumberOrString::String(s) => s,
@@ -342,14 +361,23 @@ impl Application {
 
                         if let lsp::WorkDoneProgress::End(_) = work {
                             self.lsp_progress.end_progress(server_id, &token);
+                            if !self.lsp_progress.is_progressing(server_id) {
+                                let ui = self
+                                    .compositor
+                                    .find(std::any::type_name::<ui::EditorView>())
+                                    .unwrap();
+                                if let Some(ui) = ui.as_any_mut().downcast_mut::<ui::EditorView>() {
+                                    ui.spinners_mut().get_or_create(server_id).stop();
+                                };
+                            }
                         } else {
                             self.lsp_progress.update(server_id, token, work);
                         }
 
-                        if self.lsp_progress_enabled {
+                        if self.lsp_display_messages {
                             self.editor.set_status(status);
-                            self.render();
                         }
+                        self.render();
                     }
                     _ => unreachable!(),
                 }
@@ -372,6 +400,17 @@ impl Application {
                     MethodCall::WorkDoneProgressCreate(params) => {
                         self.lsp_progress.create(server_id, params.token);
 
+                        let ui = self
+                            .compositor
+                            .find(std::any::type_name::<ui::EditorView>())
+                            .unwrap();
+                        if let Some(ui) = ui.as_any_mut().downcast_mut::<ui::EditorView>() {
+                            let spinner = ui.spinners_mut().get_or_create(server_id);
+                            if spinner.is_stopped() {
+                                spinner.start();
+                            }
+                        };
+
                         let doc = self.editor.documents().find(|doc| {
                             doc.language_server()
                                 .map(|server| server.id() == server_id)
diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs
index bcf946b7..f87a440d 100644
--- a/helix-term/src/commands.rs
+++ b/helix-term/src/commands.rs
@@ -19,7 +19,7 @@ use anyhow::anyhow;
 use helix_lsp::{
     lsp,
     util::{lsp_pos_to_pos, lsp_range_to_range, pos_to_lsp_pos, range_to_lsp_range},
-    OffsetEncoding,
+    LspProgressMap, OffsetEncoding,
 };
 use insert::*;
 use movement::Movement;
diff --git a/helix-term/src/compositor.rs b/helix-term/src/compositor.rs
index 0e6a313d..b04d4588 100644
--- a/helix-term/src/compositor.rs
+++ b/helix-term/src/compositor.rs
@@ -1,9 +1,10 @@
 // Each component declares it's own size constraints and gets fitted based on it's parent.
 // Q: how does this work with popups?
 // cursive does compositor.screen_mut().add_layer_at(pos::absolute(x, y), <component>)
+use helix_core::Position;
+use helix_lsp::LspProgressMap;
 
 use crossterm::event::Event;
-use helix_core::Position;
 use tui::{buffer::Buffer as Surface, layout::Rect, terminal::CursorKind};
 
 pub type Callback = Box<dyn FnOnce(&mut Compositor)>;
diff --git a/helix-term/src/config.rs b/helix-term/src/config.rs
index 2c95fae3..839235f1 100644
--- a/helix-term/src/config.rs
+++ b/helix-term/src/config.rs
@@ -1,35 +1,28 @@
 use anyhow::{Error, Result};
-use std::{collections::HashMap, str::FromStr};
+use std::collections::HashMap;
 
 use serde::{de::Error as SerdeError, Deserialize, Serialize};
 
 use crate::keymap::{parse_keymaps, Keymaps};
 
-pub struct GlobalConfig {
-    pub theme: Option<String>,
-    pub lsp_progress: bool,
-}
-
-impl Default for GlobalConfig {
-    fn default() -> Self {
-        Self {
-            lsp_progress: true,
-            theme: None,
-        }
-    }
-}
-
 #[derive(Default)]
 pub struct Config {
-    pub global: GlobalConfig,
+    pub theme: Option<String>,
+    pub lsp: LspConfig,
     pub keymaps: Keymaps,
 }
 
+#[derive(Default, Serialize, Deserialize)]
+pub struct LspConfig {
+    pub display_messages: bool,
+}
+
 #[derive(Serialize, Deserialize)]
 #[serde(rename_all = "kebab-case")]
 struct TomlConfig {
     theme: Option<String>,
-    lsp_progress: Option<bool>,
+    #[serde(default)]
+    lsp: LspConfig,
     keys: Option<HashMap<String, HashMap<String, String>>>,
 }
 
@@ -40,10 +33,8 @@ impl<'de> Deserialize<'de> for Config {
     {
         let config = TomlConfig::deserialize(deserializer)?;
         Ok(Self {
-            global: GlobalConfig {
-                lsp_progress: config.lsp_progress.unwrap_or(true),
-                theme: config.theme,
-            },
+            theme: config.theme,
+            lsp: config.lsp,
             keymaps: config
                 .keys
                 .map(|r| parse_keymaps(&r))
diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs
index 7f0d06e9..fcd6270e 100644
--- a/helix-term/src/ui/editor.rs
+++ b/helix-term/src/ui/editor.rs
@@ -3,7 +3,7 @@ use crate::{
     compositor::{Component, Compositor, Context, EventResult},
     key,
     keymap::{self, Keymaps},
-    ui::Completion,
+    ui::{Completion, ProgressSpinners},
 };
 
 use helix_core::{
@@ -11,6 +11,7 @@ use helix_core::{
     syntax::{self, HighlightEvent},
     Position, Range,
 };
+use helix_lsp::LspProgressMap;
 use helix_view::{document::Mode, Document, Editor, Theme, View};
 use std::borrow::Cow;
 
@@ -31,6 +32,7 @@ pub struct EditorView {
     on_next_key: Option<Box<dyn FnOnce(&mut commands::Context, KeyEvent)>>,
     last_insert: (commands::Command, Vec<KeyEvent>),
     completion: Option<Completion>,
+    spinners: ProgressSpinners,
 }
 
 const OFFSET: u16 = 7; // 1 diagnostic + 5 linenr + 1 gutter
@@ -48,9 +50,15 @@ impl EditorView {
             on_next_key: None,
             last_insert: (commands::Command::normal_mode, Vec::new()),
             completion: None,
+            spinners: ProgressSpinners::default(),
         }
     }
 
+    pub fn spinners_mut(&mut self) -> &mut ProgressSpinners {
+        &mut self.spinners
+    }
+
+    #[allow(clippy::too_many_arguments)]
     pub fn render_view(
         &self,
         doc: &Document,
@@ -458,6 +466,7 @@ impl EditorView {
         );
     }
 
+    #[allow(clippy::too_many_arguments)]
     pub fn render_statusline(
         &self,
         doc: &Document,
@@ -476,6 +485,15 @@ impl EditorView {
             Mode::Select => "SEL",
             Mode::Normal => "NOR",
         };
+        let progress = doc
+            .language_server()
+            .and_then(|srv| {
+                self.spinners
+                    .get(srv.id())
+                    .and_then(|spinner| spinner.frame())
+            })
+            .unwrap_or("");
+
         let style = if is_focused {
             theme.get("ui.statusline")
         } else {
@@ -486,13 +504,14 @@ impl EditorView {
         if is_focused {
             surface.set_string(viewport.x + 1, viewport.y, mode, style);
         }
+        surface.set_string(viewport.x + 5, viewport.y, progress, style);
 
         if let Some(path) = doc.relative_path() {
             let path = path.to_string_lossy();
 
             let title = format!("{}{}", path, if doc.is_modified() { "[+]" } else { "" });
             surface.set_stringn(
-                viewport.x + 6,
+                viewport.x + 8,
                 viewport.y,
                 title,
                 viewport.width.saturating_sub(6) as usize,