From 7c4fdc03216e2d22a80fe90c812c071aaac67bde Mon Sep 17 00:00:00 2001 From: Daniel Date: Thu, 20 Aug 2026 16:42:42 +0000 Subject: [PATCH] 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` vs `Option` 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 --- assets/settings/default.json | 17 +- crates/codestral/src/codestral.rs | 14 +- .../src/copilot_edit_prediction_delegate.rs | 59 +++-- crates/edit_prediction/src/edit_prediction.rs | 54 ++-- .../src/edit_prediction_tests.rs | 81 ++++++ .../src/zed_edit_prediction_delegate.rs | 5 +- .../src/edit_prediction_types.rs | 10 +- crates/editor/src/edit_prediction.rs | 12 +- crates/editor/src/edit_prediction_tests.rs | 5 +- crates/language/src/language_settings.rs | 74 +++++- crates/settings_content/src/language.rs | 50 +++- .../settings_content/src/settings_content.rs | 15 +- .../pages/edit_prediction_provider_setup.rs | 246 +++++++++++++++++- docs/src/ai/edit-prediction.md | 28 ++ docs/src/reference/all-settings.md | 23 ++ 15 files changed, 621 insertions(+), 72 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index 7e71a8cb0a..6af396c37b 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -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. diff --git a/crates/codestral/src/codestral.rs b/crates/codestral/src/codestral.rs index c5889ae666..c495167593 100644 --- a/crates/codestral/src/codestral.rs +++ b/crates/codestral/src/codestral.rs @@ -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 = env_var!("CODESTRAL_API_KEY"); @@ -223,11 +222,14 @@ impl EditPredictionDelegate for CodestralEditPredictionDelegate { &mut self, buffer: Entity, cursor_position: language::Anchor, - debounce: bool, + debounce_duration: Duration, _trigger: EditPredictionRequestTrigger, cx: &mut Context, ) { - 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); diff --git a/crates/copilot/src/copilot_edit_prediction_delegate.rs b/crates/copilot/src/copilot_edit_prediction_delegate.rs index 85446c4792..dfb8ebcdb7 100644 --- a/crates/copilot/src/copilot_edit_prediction_delegate.rs +++ b/crates/copilot/src/copilot_edit_prediction_delegate.rs @@ -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>>, @@ -77,16 +75,14 @@ impl EditPredictionDelegate for CopilotEditPredictionDelegate { &mut self, buffer: Entity, cursor_position: language::Anchor, - debounce: bool, + debounce_duration: Duration, _trigger: EditPredictionRequestTrigger, cx: &mut Context, ) { 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); + }); }); }); } diff --git a/crates/edit_prediction/src/edit_prediction.rs b/crates/edit_prediction/src/edit_prediction.rs index 82f94028be..650bc9173a 100644 --- a/crates/edit_prediction/src/edit_prediction.rs +++ b/crates/edit_prediction/src/edit_prediction.rs @@ -2305,6 +2305,7 @@ impl EditPredictionStore { project: Entity, buffer: Entity, position: language::Anchor, + debounce_duration: Duration, trigger: EditPredictionRequestTrigger, cx: &mut Context, ) { @@ -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, throttle_entity: EntityId, + debounce_duration: Duration, cx: &mut Context, do_refresh: impl FnOnce( WeakEntity, @@ -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); diff --git a/crates/edit_prediction/src/edit_prediction_tests.rs b/crates/edit_prediction/src/edit_prediction_tests.rs index ba78459d30..ddd23aeb64 100644 --- a/crates/edit_prediction/src/edit_prediction_tests.rs +++ b/crates/edit_prediction/src/edit_prediction_tests.rs @@ -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() diff --git a/crates/edit_prediction/src/zed_edit_prediction_delegate.rs b/crates/edit_prediction/src/zed_edit_prediction_delegate.rs index b7ae7d629e..9984a8255b 100644 --- a/crates/edit_prediction/src/zed_edit_prediction_delegate.rs +++ b/crates/edit_prediction/src/zed_edit_prediction_delegate.rs @@ -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, cursor_position: language::Anchor, - _debounce: bool, + debounce_duration: Duration, trigger: EditPredictionRequestTrigger, cx: &mut Context, ) { @@ -168,6 +168,7 @@ impl EditPredictionDelegate for ZedEditPredictionDelegate { self.project.clone(), buffer, cursor_position, + debounce_duration, trigger, cx, ) diff --git a/crates/edit_prediction_types/src/edit_prediction_types.rs b/crates/edit_prediction_types/src/edit_prediction_types.rs index 0a8f3de7dd..ec782018c0 100644 --- a/crates/edit_prediction_types/src/edit_prediction_types.rs +++ b/crates/edit_prediction_types/src/edit_prediction_types.rs @@ -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, cursor_position: language::Anchor, - debounce: bool, + debounce_duration: Duration, trigger: EditPredictionRequestTrigger, cx: &mut Context, ); @@ -239,7 +239,7 @@ pub trait EditPredictionDelegateHandle { &self, buffer: Entity, cursor_position: language::Anchor, - debounce: bool, + debounce_duration: Duration, trigger: EditPredictionRequestTrigger, cx: &mut App, ); @@ -315,12 +315,12 @@ where &self, buffer: Entity, 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) }) } diff --git a/crates/editor/src/edit_prediction.rs b/crates/editor/src/edit_prediction.rs index 68a9ac67cd..12cf7c111e 100644 --- a/crates/editor/src/edit_prediction.rs +++ b/crates/editor/src/edit_prediction.rs @@ -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, ); diff --git a/crates/editor/src/edit_prediction_tests.rs b/crates/editor/src/edit_prediction_tests.rs index 21922ce7ae..ce3dc7664a 100644 --- a/crates/editor/src/edit_prediction_tests.rs +++ b/crates/editor/src/edit_prediction_tests.rs @@ -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, _cursor_position: language::Anchor, - _debounce: bool, + _debounce_duration: Duration, _trigger: edit_prediction_types::EditPredictionRequestTrigger, _cx: &mut gpui::Context, ) { @@ -1876,7 +1877,7 @@ impl EditPredictionDelegate for FakeNonZedEditPredictionDelegate { &mut self, _buffer: gpui::Entity, _cursor_position: language::Anchor, - _debounce: bool, + _debounce_duration: Duration, _trigger: edit_prediction_types::EditPredictionRequestTrigger, _cx: &mut gpui::Context, ) { diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index 3f39b8b8fe..7a492279e3 100644 --- a/crates/language/src/language_settings.rs +++ b/crates/language/src/language_settings.rs @@ -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, + /// Settings specific to using custom OpenAI-compatible servers for edit prediction. pub open_ai_compatible_api: Option, + /// 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, /// Whether the Copilot Next Edit Suggestions feature is enabled. pub enable_next_edit_suggestions: Option, + /// Automatic prediction debounce delay. + pub prediction_debounce: DelayMs, } #[derive(Clone, Debug, Default)] @@ -526,6 +567,21 @@ pub struct CodestralSettings { pub max_tokens: Option, /// Custom API URL to use for Codestral. pub api_url: Option, + /// 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, (GlobSet, Vec)> = 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, diff --git a/crates/settings_content/src/language.rs b/crates/settings_content/src/language.rs index 45ac3c203b..d4c8223417 100644 --- a/crates/settings_content/src/language.rs +++ b/crates/settings_content/src/language.rs @@ -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, /// Settings specific to using custom OpenAI-compatible servers for edit prediction. pub open_ai_compatible_api: Option, + /// Settings specific to Zed's Edit Predictions provider. + pub zed: Option, + /// Settings specific to the Mercury Edit Predictions provider. + pub mercury: Option, /// 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, + /// 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, } #[derive( @@ -220,6 +231,11 @@ pub struct CopilotSettingsContent { /// /// Default: true pub enable_next_edit_suggestions: Option, + /// 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, } #[with_fallible_options] @@ -237,6 +253,33 @@ pub struct CodestralSettingsContent { /// /// Default: "https://codestral.mistral.ai" pub api_url: Option, + /// 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, +} + +/// 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, +} + +/// 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, } /// Ollama model name for edit predictions. @@ -283,6 +326,11 @@ pub struct OllamaEditPredictionSettingsContent { /// /// Default: "" pub prompt_format: Option, + /// 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, } /// Controls whether Zed collects training data when using Zed's Edit Predictions. diff --git a/crates/settings_content/src/settings_content.rs b/crates/settings_content/src/settings_content.rs index ee33c90e01..20738c73f8 100644 --- a/crates/settings_content/src/settings_content.rs +++ b/crates/settings_content/src/settings_content.rs @@ -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 { + s.trim() + .strip_suffix("ms") + .unwrap_or(s.trim()) + .parse::() + .map(DelayMs) + .with_context(|| format!("failed to parse delay duration: {s}")) + } +} diff --git a/crates/settings_ui/src/pages/edit_prediction_provider_setup.rs b/crates/settings_ui/src/pages/edit_prediction_provider_setup.rs index 01550cd16e..5cc7715330 100644 --- a/crates/settings_ui/src/pages/edit_prediction_provider_setup.rs +++ b/crates/settings_ui/src/pages/edit_prediction_provider_setup.rs @@ -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 { +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, +) -> 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, +) -> Option { 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 Option.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.