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.