diff options
-rw-r--r-- | .github/ISSUE_TEMPLATE/feature_request.md | 2 | ||||
-rw-r--r-- | .github/workflows/release.yml | 2 | ||||
-rw-r--r-- | Cargo.lock | 4 | ||||
-rw-r--r-- | book/src/configuration.md | 5 | ||||
-rw-r--r-- | helix-lsp/Cargo.toml | 2 | ||||
-rw-r--r-- | helix-lsp/src/lib.rs | 4 | ||||
-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 | ||||
-rw-r--r-- | helix-view/src/clipboard.rs | 2 | ||||
-rw-r--r-- | languages.toml | 2 |
15 files changed, 169 insertions, 42 deletions
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 43ba412b..9a88cb4a 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -2,7 +2,7 @@ name: Feature request about: Suggest a new feature or improvement title: '' -labels: C-enchancement +labels: C-enhancement assignees: '' --- diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index afb9a7b0..467ccbf4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -100,7 +100,7 @@ jobs: cp "target/${{ matrix.target }}/release/hx" "dist/" fi - - uses: actions/upload-artifact@v2.2.3 + - uses: actions/upload-artifact@v2.2.4 with: name: bins-${{ matrix.build }} path: dist @@ -919,9 +919,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.6.1" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a38d31d7831c6ed7aad00aa4c12d9375fd225a6dd77da1d25b707346319a975" +checksum = "5fb2ed024293bb19f7a5dc54fe83bf86532a44c12a2bb8ba40d64a4509395ca2" dependencies = [ "autocfg", "bytes", diff --git a/book/src/configuration.md b/book/src/configuration.md index 087d3fbb..5ca23911 100644 --- a/book/src/configuration.md +++ b/book/src/configuration.md @@ -4,7 +4,8 @@ To override global configuration parameters create a `config.toml` file located ## LSP -To disable language server progress report from being displayed in the status bar add this option to your `config.toml`: +To display all language server messages in the status line add the following to your `config.toml`: ```toml -lsp-progress = false +[lsp] +display-messages = true ``` diff --git a/helix-lsp/Cargo.toml b/helix-lsp/Cargo.toml index 2c1b813d..80025698 100644 --- a/helix-lsp/Cargo.toml +++ b/helix-lsp/Cargo.toml @@ -23,5 +23,5 @@ lsp-types = { version = "0.89", features = ["proposed"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" thiserror = "1.0" -tokio = { version = "1.6", features = ["full"] } +tokio = { version = "1.7", features = ["full"] } tokio-stream = "0.1.6" 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..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() + } +} diff --git a/helix-view/src/clipboard.rs b/helix-view/src/clipboard.rs index dcc44340..e32933e3 100644 --- a/helix-view/src/clipboard.rs +++ b/helix-view/src/clipboard.rs @@ -36,7 +36,7 @@ pub fn get_clipboard_provider() -> Box<dyn ClipboardProvider> { } else if env_var_is_set("WAYLAND_DISPLAY") && exists("wl-copy") && exists("wl-paste") { command_provider! { paste => "wl-paste", "--no-newline"; - copy => "wl-copy", "--foreground", "--type", "text/plain"; + copy => "wl-copy", "--type", "text/plain"; } } else if env_var_is_set("DISPLAY") && exists("xclip") { command_provider! { diff --git a/languages.toml b/languages.toml index 9ae7a658..f7564c88 100644 --- a/languages.toml +++ b/languages.toml @@ -116,7 +116,7 @@ roots = [] language-server = { command = "pyls" } # TODO: pyls needs utf-8 offsets -indent = { tab-width = 2, unit = " " } +indent = { tab-width = 4, unit = " " } [[language]] name = "nix" |