diff options
Diffstat (limited to 'helix-term')
-rw-r--r-- | helix-term/src/application.rs | 50 | ||||
-rw-r--r-- | helix-term/src/commands.rs | 2 | ||||
-rw-r--r-- | helix-term/src/compositor.rs | 3 | ||||
-rw-r--r-- | helix-term/src/config.rs | 33 | ||||
-rw-r--r-- | helix-term/src/ui/editor.rs | 23 | ||||
-rw-r--r-- | helix-term/src/ui/mod.rs | 2 | ||||
-rw-r--r-- | helix-term/src/ui/spinner.rs | 75 |
7 files changed, 155 insertions, 33 deletions
diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 08853ed0..f06ccff2 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, @@ -37,12 +44,13 @@ pub struct Application { compositor: Compositor, editor: Editor, + config: Config, + theme_loader: Arc<theme::Loader>, syn_loader: Arc<syntax::Loader>, - callbacks: LspCallbacks, + callbacks: LspCallbacks, lsp_progress: LspProgressMap, - lsp_progress_enabled: bool, } impl Application { @@ -62,7 +70,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) => { @@ -79,7 +87,7 @@ impl Application { let mut editor = Editor::new(size, theme_loader.clone(), syn_loader.clone()); - let mut editor_view = Box::new(ui::EditorView::new(config.keymaps)); + let mut editor_view = Box::new(ui::EditorView::new(config.keymaps.clone())); compositor.push(editor_view); if !args.files.is_empty() { @@ -108,11 +116,13 @@ impl Application { compositor, editor, + config, + theme_loader, syn_loader, + callbacks: FuturesUnordered::new(), lsp_progress: LspProgressMap::new(), - lsp_progress_enabled: config.global.lsp_progress, }; Ok(app) @@ -198,6 +208,15 @@ impl Application { server_id: usize, ) { use helix_lsp::{Call, MethodCall, Notification}; + let editor_view = self + .compositor + .find(std::any::type_name::<ui::EditorView>()) + .expect("expected at least one EditorView"); + let editor_view = editor_view + .as_any_mut() + .downcast_mut::<ui::EditorView>() + .unwrap(); + match call { Call::Notification(helix_lsp::jsonrpc::Notification { method, params, .. }) => { let notification = match Notification::parse(&method, params) { @@ -305,11 +324,18 @@ impl Application { (None, message, &None) } else { self.lsp_progress.end_progress(server_id, &token); + if !self.lsp_progress.is_progressing(server_id) { + editor_view.spinners_mut().get_or_create(server_id).stop(); + } self.editor.clear_status(); + + // we want to render to clear any leftover spinners or messages + self.render(); return; } } }; + let token_d: &dyn std::fmt::Display = match &token { lsp::NumberOrString::Number(n) => n, lsp::NumberOrString::String(s) => s, @@ -342,14 +368,17 @@ impl Application { if let lsp::WorkDoneProgress::End(_) = work { self.lsp_progress.end_progress(server_id, &token); + if !self.lsp_progress.is_progressing(server_id) { + editor_view.spinners_mut().get_or_create(server_id).stop(); + } } else { self.lsp_progress.update(server_id, token, work); } - if self.lsp_progress_enabled { + if self.config.lsp.display_messages { self.editor.set_status(status); - self.render(); } + self.render(); } _ => unreachable!(), } @@ -372,6 +401,11 @@ impl Application { MethodCall::WorkDoneProgressCreate(params) => { self.lsp_progress.create(server_id, params.token); + let spinner = editor_view.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 63a4d901..8866b79b 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -22,7 +22,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 faede58c..44f331ff 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}, LineEnding, 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, diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index e0177b7c..c062bffe 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -5,6 +5,7 @@ mod menu; mod picker; mod popup; mod prompt; +mod spinner; mod text; pub use completion::Completion; @@ -14,6 +15,7 @@ pub use menu::Menu; pub use picker::Picker; pub use popup::Popup; pub use prompt::{Prompt, PromptEvent}; +pub use spinner::{ProgressSpinners, Spinner}; pub use text::Text; pub use tui::layout::Rect; diff --git a/helix-term/src/ui/spinner.rs b/helix-term/src/ui/spinner.rs new file mode 100644 index 00000000..e8a43b48 --- /dev/null +++ b/helix-term/src/ui/spinner.rs @@ -0,0 +1,75 @@ +use std::{collections::HashMap, time::SystemTime}; + +#[derive(Default, Debug)] +pub struct ProgressSpinners { + inner: HashMap<usize, Spinner>, +} + +impl ProgressSpinners { + pub fn get(&self, id: usize) -> Option<&Spinner> { + self.inner.get(&id) + } + + pub fn get_or_create(&mut self, id: usize) -> &mut Spinner { + self.inner.entry(id).or_insert_with(Spinner::default) + } +} + +impl Default for Spinner { + fn default() -> Self { + Self::dots(80) + } +} + +#[derive(Debug)] +pub struct Spinner { + frames: Vec<&'static str>, + count: usize, + start: Option<SystemTime>, + interval: u64, +} + +impl Spinner { + /// Creates a new spinner with `frames` and `interval`. + /// Expects the frames count and interval to be greater than 0. + pub fn new(frames: Vec<&'static str>, interval: u64) -> Self { + let count = frames.len(); + assert!(count > 0); + assert!(interval > 0); + + Self { + frames, + count, + interval, + start: None, + } + } + + pub fn dots(interval: u64) -> Self { + Self::new(vec!["⣾", "⣽", "⣻", "⢿", "⡿", "⣟", "⣯", "⣷"], interval) + } + + pub fn start(&mut self) { + self.start = Some(SystemTime::now()); + } + + pub fn frame(&self) -> Option<&str> { + let idx = (self + .start + .map(|time| SystemTime::now().duration_since(time))? + .ok()? + .as_millis() + / self.interval as u128) as usize + % self.count; + + self.frames.get(idx).copied() + } + + pub fn stop(&mut self) { + self.start = None; + } + + pub fn is_stopped(&self) -> bool { + self.start.is_none() + } +} |