diff --git a/assets/settings/default.json b/assets/settings/default.json index 914baeede7..33efad6ceb 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -62,6 +62,9 @@ // Whether to display inline and alongside documentation for items in the // completions menu "show_completion_documentation": true, + // The debounce delay before re-querying the language server for completion + // documentation when not included in original completion list. + "completion_documentation_secondary_query_debounce": 300, // Whether to show wrap guides in the editor. Setting this to true will // show a guide at the 'preferred_line_length' value if softwrap is set to // 'preferred_line_length', and will show any additional guides as specified diff --git a/crates/editor/src/debounced_delay.rs b/crates/editor/src/debounced_delay.rs new file mode 100644 index 0000000000..b9d8ebf103 --- /dev/null +++ b/crates/editor/src/debounced_delay.rs @@ -0,0 +1,49 @@ +use std::time::Duration; + +use futures::{channel::oneshot, FutureExt}; +use gpui::{Task, ViewContext}; + +use crate::Editor; + +pub struct DebouncedDelay { + task: Option>, + cancel_channel: Option>, +} + +impl DebouncedDelay { + pub fn new() -> DebouncedDelay { + DebouncedDelay { + task: None, + cancel_channel: None, + } + } + + pub fn fire_new(&mut self, delay: Duration, cx: &mut ViewContext, func: F) + where + F: 'static + Send + FnOnce(&mut Editor, &mut ViewContext) -> Task<()>, + { + if let Some(channel) = self.cancel_channel.take() { + _ = channel.send(()); + } + + let (sender, mut receiver) = oneshot::channel::<()>(); + self.cancel_channel = Some(sender); + + let previous_task = self.task.take(); + self.task = Some(cx.spawn(move |model, mut cx| async move { + let mut timer = cx.background_executor().timer(delay).fuse(); + if let Some(previous_task) = previous_task { + previous_task.await; + } + + futures::select_biased! { + _ = receiver => return, + _ = timer => {} + } + + if let Ok(task) = model.update(&mut cx, |project, cx| (func)(project, cx)) { + task.await; + } + })); + } +} diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index b6b2bac7be..c795eecb04 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -19,6 +19,7 @@ mod editor_settings; mod element; mod inlay_hint_cache; +mod debounced_delay; mod git; mod highlight_matching_bracket; mod hover_popover; @@ -45,6 +46,7 @@ use clock::ReplicaId; use collections::{BTreeMap, Bound, HashMap, HashSet, VecDeque}; use convert_case::{Case, Casing}; use copilot::Copilot; +use debounced_delay::DebouncedDelay; pub use display_map::DisplayPoint; use display_map::*; pub use editor_settings::EditorSettings; @@ -85,7 +87,7 @@ pub use multi_buffer::{ ToPoint, }; use ordered_float::OrderedFloat; -use parking_lot::RwLock; +use parking_lot::{Mutex, RwLock}; use project::{FormatTrigger, Location, Project, ProjectPath, ProjectTransaction}; use rand::prelude::*; use rpc::proto::*; @@ -383,6 +385,7 @@ pub struct Editor { mouse_context_menu: Option, completion_tasks: Vec<(CompletionId, Task>)>, next_completion_id: CompletionId, + completion_documentation_pre_resolve_debounce: DebouncedDelay, available_code_actions: Option<(Model, Arc<[CodeAction]>)>, code_actions_task: Option>, document_highlights_task: Option>, @@ -701,6 +704,7 @@ struct CompletionsMenu { matches: Arc<[StringMatch]>, selected_item: usize, scroll_handle: UniformListScrollHandle, + selected_completion_documentation_resolve_debounce: Arc>, } impl CompletionsMenu { @@ -741,30 +745,31 @@ impl CompletionsMenu { } fn pre_resolve_completion_documentation( - &self, + completions: Arc>>, + matches: Arc<[StringMatch]>, editor: &Editor, cx: &mut ViewContext, - ) -> Option> { + ) -> Task<()> { let settings = EditorSettings::get_global(cx); if !settings.show_completion_documentation { - return None; + return Task::ready(()); } let Some(provider) = editor.completion_provider.as_ref() else { - return None; + return Task::ready(()); }; let resolve_task = provider.resolve_completions( - self.matches.iter().map(|m| m.candidate_id).collect(), - self.completions.clone(), + matches.iter().map(|m| m.candidate_id).collect(), + completions.clone(), cx, ); - return Some(cx.spawn(move |this, mut cx| async move { + return cx.spawn(move |this, mut cx| async move { if let Some(true) = resolve_task.await.log_err() { this.update(&mut cx, |_, cx| cx.notify()).ok(); } - })); + }); } fn attempt_resolve_selected_completion_documentation( @@ -785,12 +790,20 @@ impl CompletionsMenu { let resolve_task = project.update(cx, |project, cx| { project.resolve_completions(vec![completion_index], self.completions.clone(), cx) }); - cx.spawn(move |this, mut cx| async move { - if let Some(true) = resolve_task.await.log_err() { - this.update(&mut cx, |_, cx| cx.notify()).ok(); - } - }) - .detach(); + + let delay_ms = + EditorSettings::get_global(cx).completion_documentation_secondary_query_debounce; + let delay = Duration::from_millis(delay_ms); + + self.selected_completion_documentation_resolve_debounce + .lock() + .fire_new(delay, cx, |_, cx| { + cx.spawn(move |this, mut cx| async move { + if let Some(true) = resolve_task.await.log_err() { + this.update(&mut cx, |_, cx| cx.notify()).ok(); + } + }) + }); } fn visible(&self) -> bool { @@ -1434,6 +1447,7 @@ impl Editor { mouse_context_menu: None, completion_tasks: Default::default(), next_completion_id: 0, + completion_documentation_pre_resolve_debounce: DebouncedDelay::new(), next_inlay_id: 0, available_code_actions: Default::default(), code_actions_task: Default::default(), @@ -3143,7 +3157,7 @@ impl Editor { let task = cx.spawn(|this, mut cx| { async move { let completions = completions.await.log_err(); - let (menu, pre_resolve_task) = if let Some(completions) = completions { + let menu = if let Some(completions) = completions { let mut menu = CompletionsMenu { id, initial_position: position, @@ -3163,23 +3177,40 @@ impl Editor { matches: Vec::new().into(), selected_item: 0, scroll_handle: UniformListScrollHandle::new(), + selected_completion_documentation_resolve_debounce: Arc::new(Mutex::new( + DebouncedDelay::new(), + )), }; menu.filter(query.as_deref(), cx.background_executor().clone()) .await; if menu.matches.is_empty() { - (None, None) + None } else { - let pre_resolve_task = this - .update(&mut cx, |editor, cx| { - menu.pre_resolve_completion_documentation(editor, cx) - }) - .ok() - .flatten(); - (Some(menu), pre_resolve_task) + this.update(&mut cx, |editor, cx| { + let completions = menu.completions.clone(); + let matches = menu.matches.clone(); + + let delay_ms = EditorSettings::get_global(cx) + .completion_documentation_secondary_query_debounce; + let delay = Duration::from_millis(delay_ms); + + editor + .completion_documentation_pre_resolve_debounce + .fire_new(delay, cx, |editor, cx| { + CompletionsMenu::pre_resolve_completion_documentation( + completions, + matches, + editor, + cx, + ) + }); + }) + .ok(); + Some(menu) } } else { - (None, None) + None }; this.update(&mut cx, |this, cx| { @@ -3215,10 +3246,6 @@ impl Editor { } })?; - if let Some(pre_resolve_task) = pre_resolve_task { - pre_resolve_task.await; - } - Ok::<_, anyhow::Error>(()) } .log_err() diff --git a/crates/editor/src/editor_settings.rs b/crates/editor/src/editor_settings.rs index 7a5f074d44..d4d71cd2bf 100644 --- a/crates/editor/src/editor_settings.rs +++ b/crates/editor/src/editor_settings.rs @@ -8,6 +8,7 @@ pub struct EditorSettings { pub hover_popover_enabled: bool, pub show_completions_on_input: bool, pub show_completion_documentation: bool, + pub completion_documentation_secondary_query_debounce: u64, pub use_on_type_format: bool, pub scrollbar: Scrollbar, pub relative_line_numbers: bool, @@ -72,6 +73,11 @@ pub struct EditorSettingsContent { /// /// Default: true pub show_completion_documentation: Option, + /// The debounce delay before re-querying the language server for completion + /// documentation when not included in original completion list. + /// + /// Default: 300 ms + pub completion_documentation_secondary_query_debounce: Option, /// Whether to use additional LSP queries to format (and amend) the code after /// every "trigger" symbol input, defined by LSP server capabilities. /// diff --git a/crates/project/src/debounced_delay.rs b/crates/project/src/debounced_delay.rs new file mode 100644 index 0000000000..152df1ba0e --- /dev/null +++ b/crates/project/src/debounced_delay.rs @@ -0,0 +1,49 @@ +use std::time::Duration; + +use futures::{channel::oneshot, FutureExt}; +use gpui::{ModelContext, Task}; + +use crate::Project; + +pub struct DebouncedDelay { + task: Option>, + cancel_channel: Option>, +} + +impl DebouncedDelay { + pub fn new() -> DebouncedDelay { + DebouncedDelay { + task: None, + cancel_channel: None, + } + } + + pub fn fire_new(&mut self, delay: Duration, cx: &mut ModelContext, func: F) + where + F: 'static + Send + FnOnce(&mut Project, &mut ModelContext) -> Task<()>, + { + if let Some(channel) = self.cancel_channel.take() { + _ = channel.send(()); + } + + let (sender, mut receiver) = oneshot::channel::<()>(); + self.cancel_channel = Some(sender); + + let previous_task = self.task.take(); + self.task = Some(cx.spawn(move |model, mut cx| async move { + let mut timer = cx.background_executor().timer(delay).fuse(); + if let Some(previous_task) = previous_task { + previous_task.await; + } + + futures::select_biased! { + _ = receiver => return, + _ = timer => {} + } + + if let Ok(task) = model.update(&mut cx, |project, cx| (func)(project, cx)) { + task.await; + } + })); + } +} diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 50b886e44b..fc7c80c7f9 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1,3 +1,4 @@ +pub mod debounced_delay; mod ignore; pub mod lsp_command; pub mod lsp_ext_command; @@ -17,11 +18,9 @@ use client::{proto, Client, Collaborator, TypedEnvelope, UserStore}; use clock::ReplicaId; use collections::{hash_map, BTreeMap, HashMap, HashSet, VecDeque}; use copilot::Copilot; +use debounced_delay::DebouncedDelay; use futures::{ - channel::{ - mpsc::{self, UnboundedReceiver}, - oneshot, - }, + channel::mpsc::{self, UnboundedReceiver}, future::{try_join_all, Shared}, stream::FuturesUnordered, AsyncWriteExt, Future, FutureExt, StreamExt, TryFutureExt, @@ -140,7 +139,7 @@ pub struct Project { buffer_snapshots: HashMap>>, // buffer_id -> server_id -> vec of snapshots buffers_being_formatted: HashSet, buffers_needing_diff: HashSet>, - git_diff_debouncer: DelayedDebounced, + git_diff_debouncer: DebouncedDelay, nonce: u128, _maintain_buffer_languages: Task<()>, _maintain_workspace_config: Task>, @@ -154,54 +153,11 @@ pub struct Project { prettier_instances: HashMap, } -struct DelayedDebounced { - task: Option>, - cancel_channel: Option>, -} - pub enum LanguageServerToQuery { Primary, Other(LanguageServerId), } -impl DelayedDebounced { - fn new() -> DelayedDebounced { - DelayedDebounced { - task: None, - cancel_channel: None, - } - } - - fn fire_new(&mut self, delay: Duration, cx: &mut ModelContext, func: F) - where - F: 'static + Send + FnOnce(&mut Project, &mut ModelContext) -> Task<()>, - { - if let Some(channel) = self.cancel_channel.take() { - _ = channel.send(()); - } - - let (sender, mut receiver) = oneshot::channel::<()>(); - self.cancel_channel = Some(sender); - - let previous_task = self.task.take(); - self.task = Some(cx.spawn(move |project, mut cx| async move { - let mut timer = cx.background_executor().timer(delay).fuse(); - if let Some(previous_task) = previous_task { - previous_task.await; - } - - futures::select_biased! { - _ = receiver => return, - _ = timer => {} - } - - if let Ok(task) = project.update(&mut cx, |project, cx| (func)(project, cx)) { - task.await; - } - })); - } -} - struct LspBufferSnapshot { version: i32, snapshot: TextBufferSnapshot, @@ -670,7 +626,7 @@ impl Project { last_workspace_edits_by_language_server: Default::default(), buffers_being_formatted: Default::default(), buffers_needing_diff: Default::default(), - git_diff_debouncer: DelayedDebounced::new(), + git_diff_debouncer: DebouncedDelay::new(), nonce: StdRng::from_entropy().gen(), terminals: Terminals { local_handles: Vec::new(), @@ -774,7 +730,7 @@ impl Project { opened_buffers: Default::default(), buffers_being_formatted: Default::default(), buffers_needing_diff: Default::default(), - git_diff_debouncer: DelayedDebounced::new(), + git_diff_debouncer: DebouncedDelay::new(), buffer_snapshots: Default::default(), nonce: StdRng::from_entropy().gen(), terminals: Terminals { diff --git a/docs/src/configuring_zed.md b/docs/src/configuring_zed.md index 8ad075d6e7..6b65d0a137 100644 --- a/docs/src/configuring_zed.md +++ b/docs/src/configuring_zed.md @@ -622,6 +622,16 @@ These values take in the same options as the root-level settings with the same n `boolean` values +## Completion Documentation Debounce Delay + +- Description: The debounce delay before re-querying the language server for completion documentation when not included in original completion list. +- Setting: `completion_documentation_secondary_query_debounce` +- Default: `300` ms + +**Options** + +`integer` values + ## Show Copilot Suggestions - Description: Whether or not to show Copilot suggestions as you type or wait for a `copilot::Toggle`.