Add debounce for re-querying completion documentation

This commit is contained in:
Julia 2024-01-30 16:21:15 -05:00 committed by Julia
parent 634fe99fa5
commit 7cb97e57f9
7 changed files with 179 additions and 79 deletions

View file

@ -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

View file

@ -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<Task<()>>,
cancel_channel: Option<oneshot::Sender<()>>,
}
impl DebouncedDelay {
pub fn new() -> DebouncedDelay {
DebouncedDelay {
task: None,
cancel_channel: None,
}
}
pub fn fire_new<F>(&mut self, delay: Duration, cx: &mut ViewContext<Editor>, func: F)
where
F: 'static + Send + FnOnce(&mut Editor, &mut ViewContext<Editor>) -> 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;
}
}));
}
}

View file

@ -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<MouseContextMenu>,
completion_tasks: Vec<(CompletionId, Task<Option<()>>)>,
next_completion_id: CompletionId,
completion_documentation_pre_resolve_debounce: DebouncedDelay,
available_code_actions: Option<(Model<Buffer>, Arc<[CodeAction]>)>,
code_actions_task: Option<Task<()>>,
document_highlights_task: Option<Task<()>>,
@ -701,6 +704,7 @@ struct CompletionsMenu {
matches: Arc<[StringMatch]>,
selected_item: usize,
scroll_handle: UniformListScrollHandle,
selected_completion_documentation_resolve_debounce: Arc<Mutex<DebouncedDelay>>,
}
impl CompletionsMenu {
@ -741,30 +745,31 @@ impl CompletionsMenu {
}
fn pre_resolve_completion_documentation(
&self,
completions: Arc<RwLock<Box<[Completion]>>>,
matches: Arc<[StringMatch]>,
editor: &Editor,
cx: &mut ViewContext<Editor>,
) -> Option<Task<()>> {
) -> 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()

View file

@ -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<bool>,
/// 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<u64>,
/// Whether to use additional LSP queries to format (and amend) the code after
/// every "trigger" symbol input, defined by LSP server capabilities.
///

View file

@ -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<Task<()>>,
cancel_channel: Option<oneshot::Sender<()>>,
}
impl DebouncedDelay {
pub fn new() -> DebouncedDelay {
DebouncedDelay {
task: None,
cancel_channel: None,
}
}
pub fn fire_new<F>(&mut self, delay: Duration, cx: &mut ModelContext<Project>, func: F)
where
F: 'static + Send + FnOnce(&mut Project, &mut ModelContext<Project>) -> 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;
}
}));
}
}

View file

@ -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<BufferId, HashMap<LanguageServerId, Vec<LspBufferSnapshot>>>, // buffer_id -> server_id -> vec of snapshots
buffers_being_formatted: HashSet<BufferId>,
buffers_needing_diff: HashSet<WeakModel<Buffer>>,
git_diff_debouncer: DelayedDebounced,
git_diff_debouncer: DebouncedDelay,
nonce: u128,
_maintain_buffer_languages: Task<()>,
_maintain_workspace_config: Task<Result<()>>,
@ -154,54 +153,11 @@ pub struct Project {
prettier_instances: HashMap<PathBuf, PrettierInstance>,
}
struct DelayedDebounced {
task: Option<Task<()>>,
cancel_channel: Option<oneshot::Sender<()>>,
}
pub enum LanguageServerToQuery {
Primary,
Other(LanguageServerId),
}
impl DelayedDebounced {
fn new() -> DelayedDebounced {
DelayedDebounced {
task: None,
cancel_channel: None,
}
}
fn fire_new<F>(&mut self, delay: Duration, cx: &mut ModelContext<Project>, func: F)
where
F: 'static + Send + FnOnce(&mut Project, &mut ModelContext<Project>) -> 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 {

View file

@ -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`.