Add configurable inline completion debounce timeout (#61568)

# Objective

Add configurable inline completion debounce timeout
Fixes/implements #23159

## Solution

I initially wanted to make a global setting for this, but it would
conflict with hardcoded debounces in codestral (150ms) and copilot
(75ms) which I assume are there for a reason.

So I ended up using the same mechanism used for the hardcoded debounce
in Codestral (`DEBOUNCE_TIMEOUT`) and Copilot
(`COPILOT_DEBOUNCE_TIMEOUT`)
and made it accessible and configurable for all providers.


Also fixed a bug with `DelayMs` `Display` trait adding "ms" into the
input field which then fails to parse something like "150ms" as a `u64`
by implementing `FromStr` which strips the "ms" suffix if present.

So now both "1000" and "1000ms" are parsed correctly and apply.

If the parsing fix is not relevant enough I can open a separate issue +
PR for that (and the inconsistent use and therefore display of
`Option<u64>` vs `Option<DelayMs>` in other ms input fields).

## Testing

#### Did you test these changes? If so, how?

Added a separate test which passes
`test_refresh_prediction_from_buffer_honors_debounce_duration`

Manually tested with openapi compatible prediction

All other tests in affected crates pass (`cargo test -p settings_content
-p settings_ui -p editor -p edit_prediction -p language
`)

#### How can other people (reviewers) test your changes? Is there
anything specific they need to know?

Open provider settings and adjust debounce, then see how long it takes
for a prediction to render.

#### If relevant, what platforms did you test these changes on, and are
there any important ones you can't test?

Tested on Fedora 43 KDE, but it shouldn't matter as none of the affected
code is platform specific.

## Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content adheres to Zed's UI standards
([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
and
[icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md)
guidelines)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

## Showcase


https://github.com/user-attachments/assets/14efa628-765e-4c2a-ac44-01aaa3657097

---

Release Notes:

- Added configurable inline completion debounce timeout, fixes #23159

---------

Co-authored-by: Ben Kunkle <ben@zed.dev>
This commit is contained in:
DanielandBen Kunkle authored and GitHub committed 2026-08-20 16:42:42 +00:00
1 parent 5d8f8cbc67
commit 7c4fdc0321
15 files changed
+621 -72

No files matched your search

+15 -2
View File
@@ -1807,31 +1807,44 @@
// "copilot": {
// "enterprise_uri": "",
// "proxy": "",
// "proxy_no_verify": false
// "enable_next_edit_suggestions": true
// "proxy_no_verify": false,
// "enable_next_edit_suggestions": true,
// // The debounce delay in milliseconds before automatically requesting a prediction.
// // Set to 0 to request predictions immediately.
// "prediction_debounce": 75
// },
"copilot": {
"enterprise_uri": null,
"proxy": null,
"proxy_no_verify": null,
"enable_next_edit_suggestions": true,
"prediction_debounce": 75,
},
"codestral": {
"api_url": "https://codestral.mistral.ai",
"model": "codestral-latest",
"max_tokens": 150,
"prediction_debounce": 150,
},
"ollama": {
"api_url": "http://localhost:11434",
"model": "qwen2.5-coder:7b-base",
"prompt_format": "infer",
"max_output_tokens": 64,
"prediction_debounce": 0,
},
"open_ai_compatible_api": {
"api_url": "",
"model": "",
"prompt_format": "infer",
"max_output_tokens": 64,
"prediction_debounce": 0,
},
"zed": {
"prediction_debounce": 0,
},
"mercury": {
"prediction_debounce": 0,
},
// Controls whether Zed may collect training data when using Zed's Edit Predictions.
// Data is only captured when the project is detected as open source.
+8 -6
View File
@@ -22,7 +22,6 @@ use std::{
use text::ToOffset;
pub const CODESTRAL_API_URL: &str = "https://codestral.mistral.ai";
pub const DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(150);
static CODESTRAL_API_KEY_ENV_VAR: std::sync::LazyLock<EnvVar> = env_var!("CODESTRAL_API_KEY");
@@ -223,11 +222,14 @@ impl EditPredictionDelegate for CodestralEditPredictionDelegate {
&mut self,
buffer: Entity<Buffer>,
cursor_position: language::Anchor,
debounce: bool,
debounce_duration: Duration,
_trigger: EditPredictionRequestTrigger,
cx: &mut Context<Self>,
) {
log::debug!("Codestral: Refresh called (debounce: {})", debounce);
log::debug!(
"Codestral: Refresh called (debounce_duration: {:?})",
debounce_duration
);
let Some(api_key) = codestral_api_key(cx) else {
log::warn!("Codestral: No API key configured, skipping refresh");
@@ -257,9 +259,9 @@ impl EditPredictionDelegate for CodestralEditPredictionDelegate {
let api_url = codestral_api_url(cx).to_string();
self.pending_request = Some(cx.spawn(async move |this, cx| {
if debounce {
log::debug!("Codestral: Debouncing for {:?}", DEBOUNCE_TIMEOUT);
cx.background_executor().timer(DEBOUNCE_TIMEOUT).await;
if !debounce_duration.is_zero() {
log::debug!("Codestral: Debouncing for {:?}", debounce_duration);
cx.background_executor().timer(debounce_duration).await;
}
let cursor_offset = cursor_position.to_offset(&snapshot);
@@ -15,8 +15,6 @@ use icons::IconName;
use language::{Anchor, Buffer, BufferSnapshot, EditPreview, OffsetRangeExt, ToPointUtf16};
use std::{ops::Range, sync::Arc, time::Duration};
pub const COPILOT_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(75);
pub struct CopilotEditPredictionDelegate {
completion: Option<(CopilotEditPrediction, EditPreview)>,
pending_refresh: Option<Task<Result<()>>>,
@@ -77,16 +75,14 @@ impl EditPredictionDelegate for CopilotEditPredictionDelegate {
&mut self,
buffer: Entity<Buffer>,
cursor_position: language::Anchor,
debounce: bool,
debounce_duration: Duration,
_trigger: EditPredictionRequestTrigger,
cx: &mut Context<Self>,
) {
let copilot = self.copilot.clone();
self.pending_refresh = Some(cx.spawn(async move |this, cx| {
if debounce {
cx.background_executor()
.timer(COPILOT_DEBOUNCE_TIMEOUT)
.await;
if !debounce_duration.is_zero() {
cx.background_executor().timer(debounce_duration).await;
}
let completions = copilot
@@ -250,13 +246,15 @@ mod tests {
use lsp::Uri;
use project::Project;
use serde_json::json;
use settings::{AllLanguageSettingsContent, SettingsStore};
use settings::{AllLanguageSettingsContent, DelayMs, SettingsStore};
use std::future::Future;
use util::{
path,
test::{TextRangeMarker, marked_text_ranges_by},
};
const COPILOT_TEST_DEBOUNCE: Duration = Duration::from_millis(75);
#[gpui::test(iterations = 10)]
async fn test_copilot(executor: BackgroundExecutor, cx: &mut TestAppContext) {
// flaky
@@ -318,7 +316,7 @@ mod tests {
},
}],
);
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
executor.advance_clock(COPILOT_TEST_DEBOUNCE);
cx.update_editor(|editor, window, cx| {
assert!(editor.context_menu_visible());
assert!(editor.has_active_edit_prediction());
@@ -367,7 +365,7 @@ mod tests {
},
}],
);
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
executor.advance_clock(COPILOT_TEST_DEBOUNCE);
cx.update_editor(|editor, _, cx| {
assert!(!editor.context_menu_visible());
assert!(editor.has_active_edit_prediction());
@@ -399,7 +397,7 @@ mod tests {
},
}],
);
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
executor.advance_clock(COPILOT_TEST_DEBOUNCE);
cx.update_editor(|editor, window, cx| {
assert!(!editor.context_menu_visible());
assert!(editor.has_active_edit_prediction());
@@ -424,7 +422,7 @@ mod tests {
assert_eq!(editor.display_text(cx), "one.c\ntwo\nthree\n");
assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n");
});
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
executor.advance_clock(COPILOT_TEST_DEBOUNCE);
cx.editor(|editor, _, cx| {
assert!(editor.has_active_edit_prediction());
assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
@@ -532,7 +530,7 @@ mod tests {
},
}],
);
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
executor.advance_clock(COPILOT_TEST_DEBOUNCE);
cx.update_editor(|editor, window, cx| {
assert!(editor.has_active_edit_prediction());
@@ -579,7 +577,7 @@ mod tests {
},
}],
);
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
executor.advance_clock(COPILOT_TEST_DEBOUNCE);
cx.update_editor(|editor, window, cx| {
assert!(editor.has_active_edit_prediction());
@@ -662,7 +660,7 @@ mod tests {
cx.update_editor(|editor, window, cx| {
editor.show_edit_prediction(&Default::default(), window, cx)
});
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
executor.advance_clock(COPILOT_TEST_DEBOUNCE);
cx.update_editor(|editor, window, cx| {
assert!(editor.has_active_edit_prediction());
assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
@@ -670,7 +668,7 @@ mod tests {
editor.backspace(&Default::default(), window, cx);
});
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
executor.advance_clock(COPILOT_TEST_DEBOUNCE);
cx.run_until_parked();
cx.update_editor(|editor, window, cx| {
assert!(editor.has_active_edit_prediction());
@@ -679,7 +677,7 @@ mod tests {
editor.backspace(&Default::default(), window, cx);
});
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
executor.advance_clock(COPILOT_TEST_DEBOUNCE);
cx.run_until_parked();
cx.update_editor(|editor, window, cx| {
assert!(editor.has_active_edit_prediction());
@@ -760,7 +758,7 @@ mod tests {
});
editor.show_edit_prediction(&Default::default(), window, cx);
});
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
executor.advance_clock(COPILOT_TEST_DEBOUNCE);
_ = editor.update_in(cx, |editor, _, _| {
assert!(editor.has_active_edit_prediction());
});
@@ -829,7 +827,7 @@ mod tests {
);
// Ensure the new suggestion is displayed when the debounce timeout expires.
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
executor.advance_clock(COPILOT_TEST_DEBOUNCE);
_ = editor.update(cx, |editor, _| {
assert!(editor.has_active_edit_prediction());
});
@@ -906,7 +904,7 @@ mod tests {
cx.update_editor(|editor, window, cx| {
editor.show_edit_prediction(&Default::default(), window, cx)
});
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
executor.advance_clock(COPILOT_TEST_DEBOUNCE);
cx.update_editor(|editor, _, cx| {
assert!(!editor.context_menu_visible());
assert!(editor.has_active_edit_prediction());
@@ -936,7 +934,7 @@ mod tests {
},
}],
);
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
executor.advance_clock(COPILOT_TEST_DEBOUNCE);
cx.update_editor(|editor, _, cx| {
assert!(!editor.context_menu_visible());
assert!(editor.has_active_edit_prediction());
@@ -966,7 +964,7 @@ mod tests {
},
}],
);
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
executor.advance_clock(COPILOT_TEST_DEBOUNCE);
cx.update_editor(|editor, _, cx| {
assert!(editor.context_menu_visible());
assert!(editor.has_active_edit_prediction());
@@ -1081,7 +1079,7 @@ mod tests {
);
});
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
executor.advance_clock(COPILOT_TEST_DEBOUNCE);
assert!(copilot_requests.try_recv().is_err());
_ = editor.update(cx, |editor, window, cx| {
@@ -1097,7 +1095,7 @@ mod tests {
);
});
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
executor.advance_clock(COPILOT_TEST_DEBOUNCE);
assert!(copilot_requests.try_recv().is_ok());
}
@@ -1165,7 +1163,18 @@ mod tests {
cx.set_global(store);
theme_settings::init(theme::LoadThemes::JustBase, cx);
SettingsStore::update_global(cx, |store: &mut SettingsStore, cx| {
store.update_user_settings(cx, |settings| f(&mut settings.project.all_languages));
store.update_user_settings(cx, |settings| {
settings
.project
.all_languages
.edit_predictions
.get_or_insert_default()
.copilot
.get_or_insert_default()
.prediction_debounce =
Some(DelayMs(COPILOT_TEST_DEBOUNCE.as_millis() as u64));
f(&mut settings.project.all_languages);
});
});
});
}
+33 -21
View File
@@ -2305,6 +2305,7 @@ impl EditPredictionStore {
project: Entity<Project>,
buffer: Entity<Buffer>,
position: language::Anchor,
debounce_duration: Duration,
trigger: EditPredictionRequestTrigger,
cx: &mut Context<Self>,
) {
@@ -2314,29 +2315,35 @@ impl EditPredictionStore {
let trigger = predict_edits_request_trigger_from_editor_trigger(trigger);
self.queue_prediction_refresh(project.clone(), buffer.entity_id(), cx, move |this, cx| {
let Some(request_task) = this
.update(cx, |this, cx| {
this.request_prediction_internal(
project.clone(),
buffer.clone(),
position,
trigger,
cx,
)
})
.log_err()
else {
return Task::ready(anyhow::Ok(None));
};
self.queue_prediction_refresh(
project.clone(),
buffer.entity_id(),
debounce_duration,
cx,
move |this, cx| {
let Some(request_task) = this
.update(cx, |this, cx| {
this.request_prediction_internal(
project.clone(),
buffer.clone(),
position,
trigger,
cx,
)
})
.log_err()
else {
return Task::ready(anyhow::Ok(None));
};
cx.spawn(async move |_cx| {
request_task.await.map(|prediction_result| {
prediction_result
.map(|prediction_result| (prediction_result, buffer.entity_id()))
cx.spawn(async move |_cx| {
request_task.await.map(|prediction_result| {
prediction_result
.map(|prediction_result| (prediction_result, buffer.entity_id()))
})
})
})
})
},
)
}
pub const THROTTLE_TIMEOUT: Duration = Duration::from_millis(300);
@@ -2509,6 +2516,7 @@ impl EditPredictionStore {
&mut self,
project: Entity<Project>,
throttle_entity: EntityId,
debounce_duration: Duration,
cx: &mut Context<Self>,
do_refresh: impl FnOnce(
WeakEntity<Self>,
@@ -2537,6 +2545,10 @@ impl EditPredictionStore {
let throttle_at_enqueue = project_state.last_edit_prediction_refresh;
let task = cx.spawn(async move |this, cx| {
if !debounce_duration.is_zero() {
cx.background_executor().timer(debounce_duration).await;
}
let throttle_wait = this
.update(cx, |this, cx| {
let project_state = this.get_or_init_project(&project, cx);
@@ -95,6 +95,7 @@ async fn test_current_state(cx: &mut TestAppContext) {
project.clone(),
buffer1.clone(),
position,
Duration::ZERO,
EditPredictionRequestTrigger::Other,
cx,
)
@@ -132,6 +133,64 @@ async fn test_current_state(cx: &mut TestAppContext) {
});
}
#[gpui::test]
async fn test_refresh_prediction_from_buffer_honors_debounce_duration(cx: &mut TestAppContext) {
let (ep_store, mut requests) = init_test_with_fake_client(cx);
cx.update(|cx| set_jumps_feature_flag_override(cx, "on"));
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
"/root",
json!({
"foo.md": "Hello!\n"
}),
)
.await;
let project = Project::test(fs, vec![path!("/root").as_ref()], cx).await;
let buffer = project
.update(cx, |project, cx| {
let path = project.find_project_path(path!("root/foo.md"), cx).unwrap();
project.open_buffer(path, cx)
})
.await
.unwrap();
let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
let position = snapshot.anchor_before(language::Point::new(0, 0));
ep_store.update(cx, |ep_store, cx| {
ep_store.register_project(&project, cx);
ep_store.register_buffer(&buffer, &project, cx);
ep_store.refresh_prediction_from_buffer(
project.clone(),
buffer.clone(),
position,
Duration::from_millis(100),
EditPredictionRequestTrigger::Other,
cx,
);
});
cx.run_until_parked();
// The configured debounce duration should prevent the request from being sent immediately.
assert_no_predict_request_ready(&mut requests.predict_v4);
cx.background_executor
.advance_clock(Duration::from_millis(150));
cx.run_until_parked();
let (_request, respond_tx) = requests.predict_v4.next().await.unwrap();
respond_tx
.send(PredictEditsV4Response {
request_id: Uuid::new_v4().to_string(),
patch: "--- a/root/foo.md\n+++ b/root/foo.md\n@@ ... @@\n Hello!\n+world\n".to_string(),
model_version: None,
})
.unwrap();
cx.run_until_parked();
}
#[gpui::test]
async fn test_refresh_prediction_from_buffer_suppressed_while_following(cx: &mut TestAppContext) {
let (ep_store, mut requests) = init_test_with_fake_client(cx);
@@ -186,6 +245,7 @@ async fn test_refresh_prediction_from_buffer_suppressed_while_following(cx: &mut
project.clone(),
buffer.clone(),
position,
Duration::ZERO,
EditPredictionRequestTrigger::Other,
cx,
);
@@ -1298,6 +1358,7 @@ async fn test_empty_prediction(cx: &mut TestAppContext) {
project.clone(),
buffer.clone(),
position,
Duration::ZERO,
EditPredictionRequestTrigger::Explicit,
cx,
);
@@ -1384,6 +1445,7 @@ async fn test_interpolated_empty(cx: &mut TestAppContext) {
project.clone(),
buffer.clone(),
position,
Duration::ZERO,
EditPredictionRequestTrigger::Other,
cx,
);
@@ -1457,6 +1519,7 @@ async fn test_interpolate_failed(cx: &mut TestAppContext) {
project.clone(),
buffer.clone(),
position,
Duration::ZERO,
EditPredictionRequestTrigger::Other,
cx,
);
@@ -1536,6 +1599,7 @@ async fn test_replace_current(cx: &mut TestAppContext) {
project.clone(),
buffer.clone(),
position,
Duration::ZERO,
EditPredictionRequestTrigger::Other,
cx,
);
@@ -1565,6 +1629,7 @@ async fn test_replace_current(cx: &mut TestAppContext) {
project.clone(),
buffer.clone(),
position,
Duration::ZERO,
EditPredictionRequestTrigger::Other,
cx,
);
@@ -1632,6 +1697,7 @@ async fn test_current_preferred(cx: &mut TestAppContext) {
project.clone(),
buffer.clone(),
position,
Duration::ZERO,
EditPredictionRequestTrigger::Other,
cx,
);
@@ -1661,6 +1727,7 @@ async fn test_current_preferred(cx: &mut TestAppContext) {
project.clone(),
buffer.clone(),
position,
Duration::ZERO,
EditPredictionRequestTrigger::Other,
cx,
);
@@ -1747,6 +1814,7 @@ async fn test_cancel_earlier_pending_requests(cx: &mut TestAppContext) {
project.clone(),
buffer.clone(),
position,
Duration::ZERO,
EditPredictionRequestTrigger::Other,
cx,
);
@@ -1759,6 +1827,7 @@ async fn test_cancel_earlier_pending_requests(cx: &mut TestAppContext) {
project.clone(),
buffer.clone(),
position,
Duration::ZERO,
EditPredictionRequestTrigger::Other,
cx,
);
@@ -1853,6 +1922,7 @@ async fn test_cancel_second_on_third_request(cx: &mut TestAppContext) {
project.clone(),
buffer.clone(),
position,
Duration::ZERO,
EditPredictionRequestTrigger::Other,
cx,
);
@@ -1865,6 +1935,7 @@ async fn test_cancel_second_on_third_request(cx: &mut TestAppContext) {
project.clone(),
buffer.clone(),
position,
Duration::ZERO,
EditPredictionRequestTrigger::Other,
cx,
);
@@ -1881,6 +1952,7 @@ async fn test_cancel_second_on_third_request(cx: &mut TestAppContext) {
project.clone(),
buffer.clone(),
position,
Duration::ZERO,
EditPredictionRequestTrigger::Other,
cx,
);
@@ -2018,6 +2090,7 @@ async fn test_cloud_timeout_backs_off_zeta_requests(cx: &mut TestAppContext) {
project.clone(),
buffer.clone(),
position,
Duration::ZERO,
EditPredictionRequestTrigger::Other,
cx,
);
@@ -2031,6 +2104,7 @@ async fn test_cloud_timeout_backs_off_zeta_requests(cx: &mut TestAppContext) {
project.clone(),
buffer.clone(),
position,
Duration::ZERO,
EditPredictionRequestTrigger::Other,
cx,
);
@@ -2051,6 +2125,7 @@ async fn test_cloud_timeout_backs_off_zeta_requests(cx: &mut TestAppContext) {
project.clone(),
buffer.clone(),
position,
Duration::ZERO,
EditPredictionRequestTrigger::Other,
cx,
);
@@ -2092,6 +2167,7 @@ async fn test_same_frame_duplicate_requests_deduplicated(cx: &mut TestAppContext
project.clone(),
buffer.clone(),
position,
Duration::ZERO,
EditPredictionRequestTrigger::Other,
cx,
);
@@ -2099,6 +2175,7 @@ async fn test_same_frame_duplicate_requests_deduplicated(cx: &mut TestAppContext
project.clone(),
buffer.clone(),
position,
Duration::ZERO,
EditPredictionRequestTrigger::Other,
cx,
);
@@ -3004,6 +3081,7 @@ async fn test_edit_prediction_v4_end_of_buffer(cx: &mut TestAppContext) {
project.clone(),
buffer.clone(),
position,
Duration::ZERO,
EditPredictionRequestTrigger::Other,
cx,
);
@@ -3065,6 +3143,7 @@ async fn test_edit_prediction_no_spurious_trailing_newline(cx: &mut TestAppConte
project.clone(),
buffer.clone(),
position,
Duration::ZERO,
EditPredictionRequestTrigger::Other,
cx,
);
@@ -3135,6 +3214,7 @@ async fn test_v3_prediction_strips_cursor_marker_from_edit_text(cx: &mut TestApp
project.clone(),
buffer.clone(),
position,
Duration::ZERO,
EditPredictionRequestTrigger::Other,
cx,
);
@@ -3388,6 +3468,7 @@ async fn make_sweep_prompt_test_ep_store(
settings::EditPredictionPromptFormatContent::Sweep,
),
max_output_tokens: Some(64),
prediction_debounce: None,
},
),
..Default::default()
@@ -1,4 +1,4 @@
use std::{cmp, sync::Arc};
use std::{cmp, sync::Arc, time::Duration};
use client::{Client, UserStore};
use cloud_llm_client::EditPredictionRejectReason;
@@ -142,7 +142,7 @@ impl EditPredictionDelegate for ZedEditPredictionDelegate {
&mut self,
buffer: Entity<language::Buffer>,
cursor_position: language::Anchor,
_debounce: bool,
debounce_duration: Duration,
trigger: EditPredictionRequestTrigger,
cx: &mut Context<Self>,
) {
@@ -168,6 +168,7 @@ impl EditPredictionDelegate for ZedEditPredictionDelegate {
self.project.clone(),
buffer,
cursor_position,
debounce_duration,
trigger,
cx,
)
@@ -1,4 +1,4 @@
use std::{ops::Range, sync::Arc};
use std::{ops::Range, sync::Arc, time::Duration};
use client::EditPredictionUsage;
use gpui::{App, Context, Entity, SharedString};
@@ -202,7 +202,7 @@ pub trait EditPredictionDelegate: 'static + Sized {
&mut self,
buffer: Entity<Buffer>,
cursor_position: language::Anchor,
debounce: bool,
debounce_duration: Duration,
trigger: EditPredictionRequestTrigger,
cx: &mut Context<Self>,
);
@@ -239,7 +239,7 @@ pub trait EditPredictionDelegateHandle {
&self,
buffer: Entity<Buffer>,
cursor_position: language::Anchor,
debounce: bool,
debounce_duration: Duration,
trigger: EditPredictionRequestTrigger,
cx: &mut App,
);
@@ -315,12 +315,12 @@ where
&self,
buffer: Entity<Buffer>,
cursor_position: language::Anchor,
debounce: bool,
debounce_duration: Duration,
trigger: EditPredictionRequestTrigger,
cx: &mut App,
) {
self.update(cx, |this, cx| {
this.refresh(buffer, cursor_position, debounce, trigger, cx)
this.refresh(buffer, cursor_position, debounce_duration, trigger, cx)
})
}
+11 -1
View File
@@ -265,10 +265,20 @@ impl Editor {
return None;
}
let debounce_duration = if debounce {
let registered_provider = self.edit_prediction_provider.as_ref()?;
let settings = all_language_settings(None, cx);
settings
.edit_predictions
.debounce_for_delegate(registered_provider.provider.name())
} else {
Duration::ZERO
};
self.edit_prediction_provider()?.refresh(
buffer,
cursor_buffer_position,
debounce,
debounce_duration,
trigger,
cx,
);
+3 -2
View File
@@ -20,6 +20,7 @@ use std::{
Arc,
atomic::{self, AtomicUsize},
},
time::Duration,
};
use text::{Point, ToOffset};
use ui::prelude::*;
@@ -1797,7 +1798,7 @@ impl EditPredictionDelegate for FakeEditPredictionDelegate {
&mut self,
_buffer: gpui::Entity<language::Buffer>,
_cursor_position: language::Anchor,
_debounce: bool,
_debounce_duration: Duration,
_trigger: edit_prediction_types::EditPredictionRequestTrigger,
_cx: &mut gpui::Context<Self>,
) {
@@ -1876,7 +1877,7 @@ impl EditPredictionDelegate for FakeNonZedEditPredictionDelegate {
&mut self,
_buffer: gpui::Entity<language::Buffer>,
_cursor_position: language::Anchor,
_debounce: bool,
_debounce_duration: Duration,
_trigger: edit_prediction_types::EditPredictionRequestTrigger,
_cx: &mut gpui::Context<Self>,
) {
+72 -2
View File
@@ -13,7 +13,7 @@ use ec4rs::{
use globset::{Glob, GlobMatcher, GlobSet, GlobSetBuilder};
use gpui::{App, Modifiers, SharedString};
use itertools::Itertools;
use settings::{DocumentFoldingRanges, DocumentSymbols, IntoGpui, SemanticTokens};
use settings::{DelayMs, DocumentFoldingRanges, DocumentSymbols, IntoGpui, SemanticTokens};
pub use settings::{
AutoIndentMode, CompletionSettingsContent, ConfiguredLanguageServer,
@@ -24,7 +24,7 @@ pub use settings::{
};
use settings::{RegisterSetting, Settings, SettingsLocation, SettingsStore, merge_from::MergeFrom};
use shellexpand;
use std::{borrow::Cow, num::NonZeroU32, path::Path, sync::Arc};
use std::{borrow::Cow, num::NonZeroU32, path::Path, sync::Arc, time::Duration};
use text::ToOffset;
/// Returns the settings for all languages from the provided file.
@@ -478,7 +478,12 @@ pub struct EditPredictionSettings {
pub codestral: CodestralSettings,
/// Settings specific to Ollama.
pub ollama: Option<OpenAiCompatibleEditPredictionSettings>,
/// Settings specific to using custom OpenAI-compatible servers for edit prediction.
pub open_ai_compatible_api: Option<OpenAiCompatibleEditPredictionSettings>,
/// Settings specific to Zed's Edit Predictions provider.
pub zed: ZedEditPredictionSettings,
/// Settings specific to the Mercury Edit Predictions provider.
pub mercury: MercuryEditPredictionSettings,
/// Controls whether training data collection is enabled.
///
/// `Default` means the value stored in the legacy KV store is used as a fallback,
@@ -498,6 +503,40 @@ impl EditPredictionSettings {
}
})
}
/// Returns the configured debounce delay for the given provider.
pub fn debounce_for(&self, provider: settings::EditPredictionProvider) -> Duration {
let delay = match provider {
settings::EditPredictionProvider::Copilot => self.copilot.prediction_debounce,
settings::EditPredictionProvider::Codestral => self.codestral.prediction_debounce,
settings::EditPredictionProvider::Ollama => self
.ollama
.as_ref()
.map_or_else(DelayMs::default, |settings| settings.prediction_debounce),
settings::EditPredictionProvider::OpenAiCompatibleApi => self
.open_ai_compatible_api
.as_ref()
.map_or_else(DelayMs::default, |settings| settings.prediction_debounce),
settings::EditPredictionProvider::Zed => self.zed.prediction_debounce,
settings::EditPredictionProvider::Mercury => self.mercury.prediction_debounce,
settings::EditPredictionProvider::None => DelayMs::default(),
};
Duration::from_millis(delay.0)
}
/// Returns the configured debounce delay for the active prediction delegate.
///
/// The Zed edit-prediction delegate handles multiple settings providers
/// (Zed, Mercury, Ollama, OpenAI-compatible), so it is identified by name
/// and then uses the currently configured provider to resolve the delay.
pub fn debounce_for_delegate(&self, delegate_name: &str) -> Duration {
match delegate_name {
"copilot" => Duration::from_millis(self.copilot.prediction_debounce.0),
"codestral" => Duration::from_millis(self.codestral.prediction_debounce.0),
"zed-predict" => self.debounce_for(self.provider),
_ => Duration::ZERO,
}
}
}
#[derive(Clone, Debug)]
@@ -516,6 +555,8 @@ pub struct CopilotSettings {
pub enterprise_uri: Option<String>,
/// Whether the Copilot Next Edit Suggestions feature is enabled.
pub enable_next_edit_suggestions: Option<bool>,
/// Automatic prediction debounce delay.
pub prediction_debounce: DelayMs,
}
#[derive(Clone, Debug, Default)]
@@ -526,6 +567,21 @@ pub struct CodestralSettings {
pub max_tokens: Option<u32>,
/// Custom API URL to use for Codestral.
pub api_url: Option<String>,
/// Automatic prediction debounce delay.
pub prediction_debounce: DelayMs,
}
#[derive(Clone, Debug, Default)]
pub struct ZedEditPredictionSettings {
/// Automatic prediction debounce delay.
pub prediction_debounce: DelayMs,
}
/// Settings specific to the Mercury Edit Predictions provider.
#[derive(Clone, Debug, Default)]
pub struct MercuryEditPredictionSettings {
/// Automatic prediction debounce delay.
pub prediction_debounce: DelayMs,
}
#[derive(Clone, Debug, Default)]
@@ -539,6 +595,8 @@ pub struct OpenAiCompatibleEditPredictionSettings {
/// The prompt format to use for completions. When `None`, the format
/// will be derived from the model name at request time.
pub prompt_format: EditPredictionPromptFormat,
/// Automatic prediction debounce delay.
pub prediction_debounce: DelayMs,
}
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
@@ -848,6 +906,7 @@ impl settings::Settings for AllLanguageSettings {
proxy_no_verify: copilot.proxy_no_verify,
enterprise_uri: copilot.enterprise_uri,
enable_next_edit_suggestions: copilot.enable_next_edit_suggestions,
prediction_debounce: copilot.prediction_debounce.unwrap(),
};
let codestral = edit_predictions.codestral.unwrap();
@@ -855,6 +914,7 @@ impl settings::Settings for AllLanguageSettings {
model: codestral.model,
max_tokens: codestral.max_tokens,
api_url: codestral.api_url,
prediction_debounce: codestral.prediction_debounce.unwrap(),
};
let ollama = edit_predictions.ollama.unwrap();
@@ -866,6 +926,7 @@ impl settings::Settings for AllLanguageSettings {
max_output_tokens: ollama.max_output_tokens.unwrap(),
api_url: ollama.api_url.unwrap().into(),
prompt_format: ollama.prompt_format.unwrap().into(),
prediction_debounce: ollama.prediction_debounce.unwrap(),
});
let openai_compatible_settings = edit_predictions.open_ai_compatible_api.unwrap();
let openai_compatible_settings = openai_compatible_settings
@@ -881,7 +942,10 @@ impl settings::Settings for AllLanguageSettings {
max_output_tokens: openai_compatible_settings.max_output_tokens.unwrap(),
api_url: api_url.into(),
prompt_format: openai_compatible_settings.prompt_format.unwrap().into(),
prediction_debounce: openai_compatible_settings.prediction_debounce.unwrap(),
});
let zed_settings = edit_predictions.zed.unwrap();
let mercury_settings = edit_predictions.mercury.unwrap();
let mut file_types: FxHashMap<Arc<str>, (GlobSet, Vec<String>)> = FxHashMap::default();
@@ -923,6 +987,12 @@ impl settings::Settings for AllLanguageSettings {
codestral: codestral_settings,
ollama: ollama_settings,
open_ai_compatible_api: openai_compatible_settings,
zed: ZedEditPredictionSettings {
prediction_debounce: zed_settings.prediction_debounce.unwrap(),
},
mercury: MercuryEditPredictionSettings {
prediction_debounce: mercury_settings.prediction_debounce.unwrap(),
},
allow_data_collection: edit_predictions.allow_data_collection.unwrap_or_default(),
},
defaults: default_language_settings,
+49 -1
View File
@@ -6,7 +6,9 @@ use serde::{Deserialize, Serialize};
use settings_macros::{MergeFrom, with_fallible_options};
use std::sync::Arc;
use crate::{DocumentFoldingRanges, DocumentSymbols, ExtendingSet, SemanticTokens, merge_from};
use crate::{
DelayMs, DocumentFoldingRanges, DocumentSymbols, ExtendingSet, SemanticTokens, merge_from,
};
/// The state of the modifier keys at some point in time
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema, MergeFrom)]
@@ -139,6 +141,10 @@ pub struct EditPredictionSettingsContent {
pub ollama: Option<OllamaEditPredictionSettingsContent>,
/// Settings specific to using custom OpenAI-compatible servers for edit prediction.
pub open_ai_compatible_api: Option<CustomEditPredictionProviderSettingsContent>,
/// Settings specific to Zed's Edit Predictions provider.
pub zed: Option<ZedEditPredictionSettingsContent>,
/// Settings specific to the Mercury Edit Predictions provider.
pub mercury: Option<MercuryEditPredictionSettingsContent>,
/// Controls whether Zed may collect training data when using Zed's Edit Predictions.
/// Data is only ever captured for files in projects that are detected as open source.
///
@@ -168,6 +174,11 @@ pub struct CustomEditPredictionProviderSettingsContent {
///
/// Default: 256
pub max_output_tokens: Option<u32>,
/// The debounce delay in milliseconds before automatically requesting a prediction
/// after typing stops. Set to 0 to request predictions immediately.
///
/// Default: 0
pub prediction_debounce: Option<DelayMs>,
}
#[derive(
@@ -220,6 +231,11 @@ pub struct CopilotSettingsContent {
///
/// Default: true
pub enable_next_edit_suggestions: Option<bool>,
/// The debounce delay in milliseconds before automatically requesting a prediction
/// after typing stops. Set to 0 to request predictions immediately.
///
/// Default: 75
pub prediction_debounce: Option<DelayMs>,
}
#[with_fallible_options]
@@ -237,6 +253,33 @@ pub struct CodestralSettingsContent {
///
/// Default: "https://codestral.mistral.ai"
pub api_url: Option<String>,
/// The debounce delay in milliseconds before automatically requesting a prediction
/// after typing stops. Set to 0 to request predictions immediately.
///
/// Default: 150
pub prediction_debounce: Option<DelayMs>,
}
/// Settings specific to Zed's Edit Predictions provider.
#[with_fallible_options]
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq)]
pub struct ZedEditPredictionSettingsContent {
/// The debounce delay in milliseconds before automatically requesting a prediction
/// after typing stops. Set to 0 to request predictions immediately.
///
/// Default: 0
pub prediction_debounce: Option<DelayMs>,
}
/// Settings specific to the Mercury Edit Predictions provider.
#[with_fallible_options]
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq)]
pub struct MercuryEditPredictionSettingsContent {
/// The debounce delay in milliseconds before automatically requesting a prediction
/// after typing stops. Set to 0 to request predictions immediately.
///
/// Default: 0
pub prediction_debounce: Option<DelayMs>,
}
/// Ollama model name for edit predictions.
@@ -283,6 +326,11 @@ pub struct OllamaEditPredictionSettingsContent {
///
/// Default: ""
pub prompt_format: Option<EditPredictionPromptFormatContent>,
/// The debounce delay in milliseconds before automatically requesting a prediction
/// after typing stops. Set to 0 to request predictions immediately.
///
/// Default: 0
pub prediction_debounce: Option<DelayMs>,
}
/// Controls whether Zed collects training data when using Zed's Edit Predictions.
@@ -15,6 +15,7 @@ mod workspace;
pub use action::{ActionName, ActionWithArguments, CommandAliasTarget};
pub use agent::*;
use anyhow::Context;
pub use editor::*;
pub use extension::*;
pub use fallible_options::*;
@@ -1524,7 +1525,6 @@ impl merge_from::MergeFrom for SaturatingBool {
Deserialize,
MergeFrom,
JsonSchema,
derive_more::FromStr,
)]
#[serde(transparent)]
pub struct DelayMs(pub u64);
@@ -1540,3 +1540,16 @@ impl std::fmt::Display for DelayMs {
write!(f, "{}ms", self.0)
}
}
impl std::str::FromStr for DelayMs {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
s.trim()
.strip_suffix("ms")
.unwrap_or(s.trim())
.parse::<u64>()
.map(DelayMs)
.with_context(|| format!("failed to parse delay duration: {s}"))
}
}
@@ -31,7 +31,9 @@ pub(crate) fn render_edit_prediction_setup_page(
) -> AnyElement {
let providers = [
Some(render_provider_dropdown(window, cx)),
render_github_copilot_provider(window, cx).map(IntoElement::into_any_element),
Some(render_zed_provider(settings_window, window, cx).into_any_element()),
render_github_copilot_provider(settings_window, window, cx)
.map(IntoElement::into_any_element),
Some(
render_api_key_provider(
IconName::Inception,
@@ -41,7 +43,16 @@ pub(crate) fn render_edit_prediction_setup_page(
},
mercury_api_token(cx),
|_cx| MERCURY_CREDENTIALS_URL,
None,
Some(
settings_window
.render_sub_page_items_section(
mercury_settings().iter().enumerate(),
true,
window,
cx,
)
.into_any_element(),
),
window,
cx,
)
@@ -495,6 +506,37 @@ fn ollama_settings() -> Box<[SettingsPageItem]> {
metadata: None,
files: USER,
}),
SettingsPageItem::SettingItem(SettingItem {
title: "Prediction Debounce",
description: "Delay in milliseconds before automatically requesting a prediction after typing stops. Set to 0 to request predictions immediately.",
field: Box::new(SettingField {
organization_override: None,
pick: |settings| {
settings
.project
.all_languages
.edit_predictions
.as_ref()?
.ollama
.as_ref()?
.prediction_debounce
.as_ref()
},
write: |settings, value, _app: &App| {
settings
.project
.all_languages
.edit_predictions
.get_or_insert_default()
.ollama
.get_or_insert_default()
.prediction_debounce = value;
},
json_path: Some("edit_predictions.ollama.prediction_debounce"),
}),
metadata: None,
files: USER,
}),
])
}
@@ -630,6 +672,37 @@ fn open_ai_compatible_settings() -> Box<[SettingsPageItem]> {
metadata: None,
files: USER,
}),
SettingsPageItem::SettingItem(SettingItem {
title: "Prediction Debounce",
description: "Delay in milliseconds before automatically requesting a prediction after typing stops. Set to 0 to request predictions immediately.",
field: Box::new(SettingField {
organization_override: None,
pick: |settings| {
settings
.project
.all_languages
.edit_predictions
.as_ref()?
.open_ai_compatible_api
.as_ref()?
.prediction_debounce
.as_ref()
},
write: |settings, value, _app: &App| {
settings
.project
.all_languages
.edit_predictions
.get_or_insert_default()
.open_ai_compatible_api
.get_or_insert_default()
.prediction_debounce = value;
},
json_path: Some("edit_predictions.open_ai_compatible_api.prediction_debounce"),
}),
metadata: None,
files: USER,
}),
])
}
@@ -734,10 +807,170 @@ fn codestral_settings() -> Box<[SettingsPageItem]> {
})),
files: USER,
}),
SettingsPageItem::SettingItem(SettingItem {
title: "Prediction Debounce",
description: "Delay in milliseconds before automatically requesting a prediction after typing stops. Set to 0 to request predictions immediately.",
field: Box::new(SettingField {
organization_override: None,
pick: |settings| {
settings
.project
.all_languages
.edit_predictions
.as_ref()?
.codestral
.as_ref()?
.prediction_debounce
.as_ref()
},
write: |settings, value, _app: &App| {
settings
.project
.all_languages
.edit_predictions
.get_or_insert_default()
.codestral
.get_or_insert_default()
.prediction_debounce = value;
},
json_path: Some("edit_predictions.codestral.prediction_debounce"),
}),
metadata: None,
files: USER,
}),
])
}
fn render_github_copilot_provider(window: &mut Window, cx: &mut App) -> Option<impl IntoElement> {
fn mercury_settings() -> Box<[SettingsPageItem]> {
Box::new([SettingsPageItem::SettingItem(SettingItem {
title: "Prediction Debounce",
description: "Delay in milliseconds before automatically requesting a prediction after typing stops. Set to 0 to request predictions immediately.",
field: Box::new(SettingField {
organization_override: None,
pick: |settings| {
settings
.project
.all_languages
.edit_predictions
.as_ref()?
.mercury
.as_ref()?
.prediction_debounce
.as_ref()
},
write: |settings, value, _app: &App| {
settings
.project
.all_languages
.edit_predictions
.get_or_insert_default()
.mercury
.get_or_insert_default()
.prediction_debounce = value;
},
json_path: Some("edit_predictions.mercury.prediction_debounce"),
}),
metadata: None,
files: USER,
})])
}
fn zed_settings() -> Box<[SettingsPageItem]> {
Box::new([SettingsPageItem::SettingItem(SettingItem {
title: "Prediction Debounce",
description: "Delay in milliseconds before automatically requesting a prediction after typing stops. Set to 0 to request predictions immediately.",
field: Box::new(SettingField {
organization_override: None,
pick: |settings| {
settings
.project
.all_languages
.edit_predictions
.as_ref()?
.zed
.as_ref()?
.prediction_debounce
.as_ref()
},
write: |settings, value, _app: &App| {
settings
.project
.all_languages
.edit_predictions
.get_or_insert_default()
.zed
.get_or_insert_default()
.prediction_debounce = value;
},
json_path: Some("edit_predictions.zed.prediction_debounce"),
}),
metadata: None,
files: USER,
})])
}
fn render_zed_provider(
settings_window: &SettingsWindow,
window: &mut Window,
cx: &mut Context<SettingsWindow>,
) -> impl IntoElement {
let zed_settings = zed_settings();
let additional_fields = settings_window
.render_sub_page_items_section(zed_settings.iter().enumerate(), true, window, cx)
.into_any_element();
v_flex()
.id("zed")
.min_w_0()
.pt_8()
.gap_1p5()
.child(
SettingsSectionHeader::new("Zed Predictions")
.icon(IconName::ZedPredict)
.no_padding(true),
)
.child(div().px_neg_8().child(additional_fields))
}
fn copilot_settings() -> Box<[SettingsPageItem]> {
Box::new([SettingsPageItem::SettingItem(SettingItem {
title: "Prediction Debounce",
description: "Delay in milliseconds before automatically requesting a prediction after typing stops. Set to 0 to request predictions immediately.",
field: Box::new(SettingField {
organization_override: None,
pick: |settings| {
settings
.project
.all_languages
.edit_predictions
.as_ref()?
.copilot
.as_ref()?
.prediction_debounce
.as_ref()
},
write: |settings, value, _app: &App| {
settings
.project
.all_languages
.edit_predictions
.get_or_insert_default()
.copilot
.get_or_insert_default()
.prediction_debounce = value;
},
json_path: Some("edit_predictions.copilot.prediction_debounce"),
}),
metadata: None,
files: USER,
})])
}
fn render_github_copilot_provider(
settings_window: &SettingsWindow,
window: &mut Window,
cx: &mut Context<SettingsWindow>,
) -> Option<impl IntoElement> {
let configuration_view = window.use_state(cx, |_, cx| {
copilot_ui::ConfigurationView::new(
move |cx| {
@@ -750,6 +983,10 @@ fn render_github_copilot_provider(window: &mut Window, cx: &mut App) -> Option<i
)
});
let additional_fields = settings_window
.render_sub_page_items_section(copilot_settings().iter().enumerate(), true, window, cx)
.into_any_element();
Some(
v_flex()
.id("github-copilot")
@@ -761,6 +998,7 @@ fn render_github_copilot_provider(window: &mut Window, cx: &mut App) -> Option<i
.icon(IconName::Copilot)
.no_padding(true),
)
.child(configuration_view),
.child(configuration_view)
.child(div().px_neg_8().child(additional_fields)),
)
}
+28
View File
@@ -146,6 +146,34 @@ If you configured edit prediction keybindings before Zed `v0.229.0`, your `keyma
**Renamed context**: The `edit_prediction_conflict` context has been replaced by `edit_prediction && (showing_completions || in_leading_whitespace)`. Zed automatically migrates any bindings that used `edit_prediction_conflict`, so no changes are required on your end.
## Configuring the Prediction Debounce
The prediction debounce controls how long Zed waits after you stop typing before automatically requesting an edit prediction. Configure it in milliseconds under the settings for your selected provider:
```json [settings]
{
"edit_predictions": {
"provider": "open_ai_compatible_api",
"open_ai_compatible_api": {
"prediction_debounce": 500
}
}
}
```
The default debounce depends on the provider:
| Provider | Default |
| --------------------- | ------: |
| GitHub Copilot | 75 ms |
| Codestral | 150 ms |
| Zed | 0 ms |
| Mercury | 0 ms |
| Ollama | 0 ms |
| OpenAI-compatible API | 0 ms |
Set `prediction_debounce` to `0` to disable the additional delay. Normal request throttling can still apply. Explicitly requesting a prediction with {#action editor::ShowEditPrediction} bypasses the configured debounce.
## Disabling Automatic Edit Prediction
You can disable edit predictions at several levels, or turn them off entirely.
+23
View File
@@ -685,6 +685,29 @@ For the case of "open", regular selection behavior can be achieved by holding `a
List of `string` values.
### Prediction Debounce
- Description: How long Zed waits after you stop typing before automatically requesting an edit prediction.
- Setting: `edit_predictions.<provider>.prediction_debounce`
- Default: `75` for GitHub Copilot, `150` for Codestral, and `0` for Zed, Mercury, Ollama, and OpenAI-compatible APIs.
**Options**
Non-negative integer values representing milliseconds. Set this to `0` to disable the additional delay. Configure the value under the settings object for the selected provider:
```json [settings]
{
"edit_predictions": {
"provider": "open_ai_compatible_api",
"open_ai_compatible_api": {
"prediction_debounce": 500
}
}
}
```
See [Configuring the Prediction Debounce](../ai/edit-prediction.md#configuring-the-prediction-debounce) for more information.
## Edit Predictions Disabled in
- Description: A list of language scopes in which edit predictions should be disabled.